/* eslint-disable @typescript-eslint/no-explicit-any */
import React, { useState, useCallback, useEffect, useRef, type FormEvent, type ReactNode } from "react";
import { v4 as uuidv4 } from "uuid";

// Wrapper to force uuid to use crypto.getRandomValues() fallback instead of crypto.randomUUID()
// This ensures compatibility with non-secure (HTTP) contexts where crypto.randomUUID() is unavailable
// Note: may be able to remove this workaround with next version of uuid
const v4 = () => uuidv4({});

import { api } from "@/lib/api";
import { ChatContext, type ChatContextValue, type PendingPromptData } from "@/lib/contexts";
import { useConfigContext, useArtifacts, useAgentCards, useTaskContext, useErrorDialog, useTitleGeneration, useBackgroundTaskMonitor, useArtifactPreview, useArtifactOperations, useAuthContext } from "@/lib/hooks";
import { useProjectContext, registerProjectDeletedCallback } from "@/lib/providers";
import { getErrorMessage, fileToBase64, migrateTask, CURRENT_SCHEMA_VERSION, getApiBearerToken, internalToDisplayText } from "@/lib/utils";
import { ConfirmationDialog } from "@/lib/components/common/ConfirmationDialog";

import type {
    CancelTaskRequest,
    DataPart,
    FileAttachment,
    FilePart,
    JSONRPCErrorResponse,
    Message,
    MessageFE,
    Notification,
    Part,
    PartFE,
    SendStreamingMessageRequest,
    SendStreamingMessageSuccessResponse,
    Session,
    Task,
    TaskStatusUpdateEvent,
    TextPart,
    ArtifactPart,
    AgentCardInfo,
    Project,
    StoredTaskData,
    RAGSearchResult,
} from "@/lib/types";

const INLINE_FILE_SIZE_LIMIT_BYTES = 1 * 1024 * 1024; // 1 MB

interface ChatProviderProps {
    children: ReactNode;
}

export const ChatProvider: React.FC<ChatProviderProps> = ({ children }) => {
    const { configWelcomeMessage, persistenceEnabled, configCollectFeedback, backgroundTasksEnabled, backgroundTasksDefaultTimeoutMs, autoTitleGenerationEnabled } = useConfigContext();
    const { activeProject, setActiveProject, projects } = useProjectContext();
    const { registerTaskEarly } = useTaskContext();
    const { ErrorDialog, setError } = useErrorDialog();
    const { userInfo } = useAuthContext();

    // State Variables from useChat
    const [sessionId, setSessionId] = useState<string>("");
    const [messages, setMessages] = useState<MessageFE[]>([]);
    const [isResponding, setIsResponding] = useState<boolean>(false);

    // RAG State
    const [ragData, _setRagData] = useState<RAGSearchResult[]>([]);
    const ragDataRef = useRef<RAGSearchResult[]>([]);
    const [ragEnabled] = useState<boolean>(true);
    const [expandedDocumentFilename, setExpandedDocumentFilename] = useState<string | null>(null);

    // Wrapper to keep ref in sync with state
    const setRagData = useCallback((data: RAGSearchResult[] | ((prev: RAGSearchResult[]) => RAGSearchResult[])) => {
        _setRagData(prev => {
            const newData = typeof data === "function" ? data(prev) : data;
            ragDataRef.current = newData;
            return newData;
        });
    }, []);
    const [notifications, setNotifications] = useState<Notification[]>([]);
    const [currentTaskId, setCurrentTaskId] = useState<string | null>(null);
    const currentEventSource = useRef<EventSource | null>(null);
    const [selectedAgentName, setSelectedAgentName] = useState<string>("");
    const [isCancelling, setIsCancelling] = useState<boolean>(false); // New state for cancellation

    const savingTasksRef = useRef<Set<string>>(new Set());

    // Track isCancelling in ref to access in async callbacks
    const isCancellingRef = useRef(isCancelling);
    useEffect(() => {
        isCancellingRef.current = isCancelling;
    }, [isCancelling]);

    // Track current session id to prevent race conditions
    const currentSessionIdRef = useRef(sessionId);
    useEffect(() => {
        currentSessionIdRef.current = sessionId;
    }, [sessionId]);

    const [taskIdInSidePanel, setTaskIdInSidePanel] = useState<string | null>(null);
    const cancelTimeoutRef = useRef<NodeJS.Timeout | null>(null);
    const isFinalizing = useRef(false);
    const latestStatusText = useRef<string | null>(null);
    const sseEventSequenceRef = useRef<number>(0);
    const backgroundTasksRef = useRef<typeof backgroundTasks>([]);
    const messagesRef = useRef<MessageFE[]>([]);

    // Ref to hold the replay function - allows calling from loadSessionTasks which is defined earlier
    const replayBufferedEventsRef = useRef<((taskId: string) => Promise<boolean>) | null>(null);

    // Ref to hold handleSseMessage - allows calling from loadSessionTasks for buffer replay
    const handleSseMessageRef = useRef<((event: MessageEvent) => void) | null>(null);

    // Track if we're currently replaying buffered events
    // When true, handleSseMessage will skip save operations since the data is already persisted
    const isReplayingEventsRef = useRef(false);

    // Track query history for deep research progress timeline
    // This accumulates queries and their URLs as they come in via deep_research_progress events
    const deepResearchQueryHistoryRef = useRef<
        Map<
            string,
            Array<{
                query: string;
                timestamp: string;
                urls: Array<{ url: string; title: string; favicon: string; source_type?: string }>;
            }>
        >
    >(new Map());

    const sessionsWithAutoGeneratedTitles = useRef<Set<string>>(new Set());

    // Agents State
    const { agents, agentNameMap: agentNameDisplayNameMap, error: agentsError, isLoading: agentsLoading, refetch: agentsRefetch } = useAgentCards();

    // Chat Side Panel State
    const { artifacts, isLoading: artifactsLoading, refetch: artifactsRefetch, setArtifacts } = useArtifacts(sessionId);

    // Title Generation
    const { generateTitle } = useTitleGeneration();

    // Side Panel Control State
    const [isSidePanelCollapsed, setIsSidePanelCollapsed] = useState<boolean>(true);
    const [activeSidePanelTab, setActiveSidePanelTab] = useState<"files" | "activity" | "rag">("files");

    // Feedback State
    const [submittedFeedback, setSubmittedFeedback] = useState<Record<string, { type: "up" | "down"; text: string }>>({});

    // Pending prompt state for starting new chat with a prompt template
    const [pendingPrompt, setPendingPrompt] = useState<PendingPromptData | null>(null);

    // Running task navigation warning state
    // Used when background tasks are disabled and user tries to switch sessions while a task is running
    const [runningTaskWarningOpen, setRunningTaskWarningOpen] = useState<boolean>(false);
    const [pendingNavigationAction, setPendingNavigationAction] = useState<(() => void) | null>(null);

    // Notification Helper
    const addNotification = useCallback((message: string, type?: "success" | "info" | "warning") => {
        setNotifications(prev => {
            const existingNotification = prev.find(n => n.message === message);

            if (existingNotification) {
                return prev;
            }

            const id = Date.now().toString();
            const newNotification = { id, message, type: type || "info" };

            setTimeout(() => {
                setNotifications(current => current.filter(n => n.id !== id));
            }, 4000);

            return [...prev, newNotification];
        });
    }, []);

    // Artifact Preview
    const {
        preview: { availableVersions: previewedArtifactAvailableVersions, currentVersion: currentPreviewedVersionNumber, content: previewFileContent },
        previewArtifact,
        openPreview,
        navigateToVersion,
        closePreview,
        setPreviewByArtifact,
    } = useArtifactPreview({
        sessionId,
        projectId: activeProject?.id,
        artifacts,
        setError,
    });

    // Artifact Operations
    const {
        uploadArtifactFile,

        isDeleteModalOpen,
        artifactToDelete,
        openDeleteModal,
        closeDeleteModal,
        confirmDelete,

        isArtifactEditMode,
        setIsArtifactEditMode,
        selectedArtifactFilenames,
        setSelectedArtifactFilenames,
        isBatchDeleteModalOpen,
        setIsBatchDeleteModalOpen,
        handleDeleteSelectedArtifacts,
        confirmBatchDeleteArtifacts,

        downloadAndResolveArtifact,
    } = useArtifactOperations({
        sessionId,
        artifacts,
        setArtifacts,
        artifactsRefetch,
        addNotification,
        setError,
        previewArtifact,
        closePreview,
    });

    // Get the authenticated user's ID for background task monitoring
    const authenticatedUserId = typeof userInfo?.username === "string" ? userInfo.username : null;

    const {
        backgroundTasks,
        notifications: backgroundNotifications,
        registerBackgroundTask,
        unregisterBackgroundTask,
        updateTaskTimestamp,
        isTaskRunningInBackground,
        checkTaskStatus,
    } = useBackgroundTaskMonitor({
        userId: authenticatedUserId,
        currentSessionId: sessionId,
        onTaskCompleted: useCallback(
            async (taskId: string, taskSessionId: string) => {
                // Only show notification if user is NOT currently viewing the session where the task completed
                // This reduces noise when the user is already on the chat page seeing the results
                if (currentSessionIdRef.current !== taskSessionId) {
                    addNotification("Background task completed", "success");
                }

                // Trigger session list refresh to update background task indicators
                if (typeof window !== "undefined") {
                    window.dispatchEvent(
                        new CustomEvent("background-task-completed", {
                            detail: { taskId },
                        })
                    );
                    // Also trigger general session list refresh
                    window.dispatchEvent(new CustomEvent("new-chat-session"));
                }

                // Trigger title generation for the completed background task
                // This handles the case where the task completes while user is on a different session
                if (autoTitleGenerationEnabled) {
                    const bgTask = backgroundTasksRef.current.find(t => t.taskId === taskId);
                    let taskSessionId = bgTask?.sessionId;

                    if (!taskSessionId || taskSessionId.trim() === "") {
                        try {
                            const statusData = await api.webui.get(`/api/v1/tasks/${taskId}/status`);
                            taskSessionId = statusData?.task?.context_id || statusData?.task?.contextId;
                        } catch {
                            // Silently ignore - title generation will be skipped if session ID cannot be determined
                        }
                    }

                    // Now proceed with title generation if we have a valid session ID
                    if (taskSessionId && taskSessionId.trim() !== "" && !sessionsWithAutoGeneratedTitles.current.has(taskSessionId)) {
                        sessionsWithAutoGeneratedTitles.current.add(taskSessionId);

                        try {
                            // Use the title-data endpoint which extracts messages from chat_tasks OR SSE buffer
                            // This works even when FE wasn't watching and chat_tasks weren't saved yet
                            console.debug("[ChatProvider] Fetching title-data for background task:", taskId);
                            const titleData = await api.webui.get(`/api/v1/tasks/${taskId}/title-data`);
                            console.debug("[ChatProvider] Title-data response:", titleData);

                            const userMessageText = titleData?.user_message || "";
                            const agentResponseText = titleData?.agent_response || "";

                            if (userMessageText && agentResponseText) {
                                console.debug("[ChatProvider] Calling generateTitle for background task");
                                await generateTitle(taskSessionId, userMessageText, agentResponseText);
                            } else {
                                console.warn("[ChatProvider] Missing messages for title generation:", {
                                    hasUser: !!userMessageText,
                                    hasAgent: !!agentResponseText,
                                });
                            }
                        } catch (error) {
                            console.error("[ChatProvider] Error generating title for completed background task:", error);
                        }
                    }
                }
            },
            [addNotification, autoTitleGenerationEnabled, generateTitle]
        ),
        onTaskFailed: useCallback(
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            (taskId: string, error: string, _taskSessionId: string) => {
                // Always show error dialog for failed tasks, regardless of current session
                // Errors are important and should not be silently ignored
                setError({ title: "Background Task Failed", error });

                // Trigger session list refresh to update background task indicators
                if (typeof window !== "undefined") {
                    window.dispatchEvent(
                        new CustomEvent("background-task-completed", {
                            detail: { taskId },
                        })
                    );
                    // Also trigger general session list refresh
                    window.dispatchEvent(new CustomEvent("new-chat-session"));
                }
            },
            [setError]
        ),
    });

    // Keep refs in sync with state
    useEffect(() => {
        backgroundTasksRef.current = backgroundTasks;
    }, [backgroundTasks]);

    useEffect(() => {
        messagesRef.current = messages;
    }, [messages]);

    // Helper function to serialize a MessageFE to MessageBubble format for backend
    const serializeMessageBubble = useCallback((message: MessageFE) => {
        // Build text with artifact markers embedded
        let combinedText = "";
        const parts = message.parts || [];

        for (const part of parts) {
            if (part.kind === "text") {
                combinedText += (part as TextPart).text;
            } else if (part.kind === "artifact") {
                // Add artifact marker for artifact parts
                const artifactPart = part as ArtifactPart;
                combinedText += `«artifact_return:${artifactPart.name}»`;
            }
        }

        return {
            id: message.metadata?.messageId || `msg-${v4()}`,
            type: message.isUser ? "user" : "agent",
            text: combinedText,
            parts: message.parts,
            uploadedFiles: message.uploadedFiles?.map(f => ({
                name: f.name,
                type: f.type,
            })),
            isError: message.isError,
            displayHtml: message.displayHtml,
            contextQuote: message.contextQuote,
            contextQuoteSourceId: message.contextQuoteSourceId,
        };
    }, []);

    // Helper function to save task data to backend
    const saveTaskToBackend = useCallback(
        async (taskData: { task_id: string; user_message?: string; message_bubbles: any[]; task_metadata?: any }, overrideSessionId?: string): Promise<boolean> => {
            const effectiveSessionId = overrideSessionId || sessionId;

            if (!persistenceEnabled || !effectiveSessionId) {
                return false;
            }

            // Prevent duplicate saves (handles React Strict Mode + race conditions)
            if (savingTasksRef.current.has(taskData.task_id)) {
                return false;
            }

            // Mark as saving
            savingTasksRef.current.add(taskData.task_id);

            try {
                await api.webui.post(`/api/v1/sessions/${effectiveSessionId}/chat-tasks`, {
                    taskId: taskData.task_id,
                    userMessage: taskData.user_message,
                    messageBubbles: JSON.stringify(taskData.message_bubbles),
                    taskMetadata: taskData.task_metadata ? JSON.stringify(taskData.task_metadata) : null,
                });
                return true;
            } catch (error) {
                console.error(`Failed saving task ${taskData.task_id}:`, error);
                return false;
            } finally {
                // Always remove from saving set after a delay to handle rapid re-renders
                setTimeout(() => {
                    savingTasksRef.current.delete(taskData.task_id);
                }, 100);
            }
        },
        [sessionId, persistenceEnabled]
    );

    // Helper function to extract artifact markers and create artifact parts
    const extractArtifactMarkers = useCallback((text: string, sessionId: string, addedArtifacts: Set<string>, processedParts: any[]) => {
        const ARTIFACT_RETURN_REGEX = /«artifact_return:([^»]+)»/g;
        const ARTIFACT_REGEX = /«artifact:([^»]+)»/g;

        const createArtifactPart = (filename: string) => {
            // Strip version suffix if present (e.g., "file.md:0" -> "file.md")
            // This handles artifact markers from agents that include version numbers
            let cleanFilename = filename;
            if (filename.includes(":")) {
                const parts = filename.split(":");
                // Check if the last part is a number (version)
                const lastPart = parts[parts.length - 1];
                if (/^\d+$/.test(lastPart)) {
                    // It's a version number, remove it
                    cleanFilename = parts.slice(0, -1).join(":");
                }
            }

            return {
                kind: "artifact",
                status: "completed",
                name: cleanFilename,
                file: {
                    name: cleanFilename,
                    uri: `artifact://${sessionId}/${cleanFilename}`,
                },
            };
        };

        // Extract artifact_return markers
        let match;
        while ((match = ARTIFACT_RETURN_REGEX.exec(text)) !== null) {
            const artifactFilename = match[1];
            // Normalize filename to avoid duplicates (strip version suffix)
            const normalizedFilename = artifactFilename.includes(":") && /:\d+$/.test(artifactFilename) ? artifactFilename.substring(0, artifactFilename.lastIndexOf(":")) : artifactFilename;

            if (!addedArtifacts.has(normalizedFilename)) {
                addedArtifacts.add(normalizedFilename);
                processedParts.push(createArtifactPart(artifactFilename));
            }
        }

        // Extract artifact: markers
        while ((match = ARTIFACT_REGEX.exec(text)) !== null) {
            const artifactFilename = match[1];
            if (!addedArtifacts.has(artifactFilename)) {
                addedArtifacts.add(artifactFilename);
                processedParts.push(createArtifactPart(artifactFilename));
            }
        }
    }, []);

    // Helper function to deserialize task data to MessageFE objects
    const deserializeTaskToMessages = useCallback(
        (task: { taskId: string; messageBubbles: any[]; taskMetadata?: any; createdTime: number }, sessionId: string): MessageFE[] => {
            return task.messageBubbles.map(bubble => {
                // Process parts to handle markers and reconstruct artifact parts if needed
                const processedParts: any[] = [];
                const originalParts = bubble.parts || [{ kind: "text", text: bubble.text || "" }];

                // Track artifact names we've already added to avoid duplicates
                const addedArtifacts = new Set<string>();

                // First, check the bubble.text field for artifact markers (TaskLoggerService saves markers there)
                // This handles the case where backend saves text with markers but parts without artifacts
                if (bubble.text) {
                    extractArtifactMarkers(bubble.text, sessionId, addedArtifacts, processedParts);
                }

                for (const part of originalParts) {
                    if (part.kind === "text" && part.text) {
                        let textContent = part.text;

                        // Extract artifact markers and convert them to artifact parts
                        extractArtifactMarkers(textContent, sessionId, addedArtifacts, processedParts);

                        // Remove artifact markers from text content
                        textContent = textContent.replace(/«artifact_return:[^»]+»/g, "");
                        textContent = textContent.replace(/«artifact:[^»]+»/g, "");

                        // Remove status update markers
                        textContent = textContent.replace(/«status_update:[^»]+»\n?/g, "");

                        // Add text part if there's content
                        if (textContent.trim()) {
                            processedParts.push({ kind: "text", text: textContent });
                        }
                    } else if (part.kind === "artifact") {
                        // Only add artifact part if not already added (from markers)
                        const artifactName = part.name;
                        if (artifactName && !addedArtifacts.has(artifactName)) {
                            addedArtifacts.add(artifactName);
                            processedParts.push(part);
                        }
                        // Skip duplicate artifacts
                    } else {
                        // Keep other non-text parts as-is
                        processedParts.push(part);
                    }
                }

                return {
                    taskId: task.taskId,
                    role: bubble.type === "user" ? "user" : "agent",
                    parts: processedParts,
                    isUser: bubble.type === "user",
                    isComplete: true,
                    files: bubble.files,
                    uploadedFiles: bubble.uploadedFiles,
                    artifactNotification: bubble.artifactNotification,
                    isError: bubble.isError,
                    displayHtml: bubble.displayHtml, // Restore mention chip HTML for user messages
                    contextQuote: bubble.contextQuote, // Restore context quote for user messages
                    contextQuoteSourceId: bubble.contextQuoteSourceId, // Restore source ID for scroll-to-source
                    metadata: {
                        messageId: bubble.id,
                        sessionId: sessionId,
                        lastProcessedEventSequence: 0,
                    },
                };
            });
        },
        [extractArtifactMarkers]
    );

    // Helper function to load session tasks and reconstruct messages
    const loadSessionTasks = useCallback(
        async (sessionId: string) => {
            const data = await api.webui.get(`/api/v1/sessions/${sessionId}/chat-tasks`);

            // Check if this session is still active before processing
            if (currentSessionIdRef.current !== sessionId) {
                console.log(`Session ${sessionId} is no longer the active session: ${currentSessionIdRef.current}`);
                return;
            }

            // Parse JSON strings from backend
            const tasks = data.tasks || [];
            const parsedTasks = tasks.map((task: StoredTaskData) => ({
                ...task,
                messageBubbles: JSON.parse(task.messageBubbles),
                taskMetadata: task.taskMetadata ? JSON.parse(task.taskMetadata) : null,
            }));

            // Apply migrations to each task
            const migratedTasks = parsedTasks.map(migrateTask);

            // Deserialize all tasks to messages
            const allMessages: MessageFE[] = [];

            // Track which tasks have buffered events (need replay)
            const tasksWithBufferedEvents = new Map<string, { events_buffered: boolean; events: any[] }>();

            if (replayBufferedEventsRef.current && backgroundTasksEnabled) {
                try {
                    // Use include_events=true to get all events in one batch
                    const response = await api.webui.get(`/api/v1/sessions/${sessionId}/events/unconsumed?include_events=true`);
                    if (response.has_events && response.events_by_task) {
                        // Populate the map from the batched response
                        for (const [taskId, taskEvents] of Object.entries(response.events_by_task)) {
                            const typedEvents = taskEvents as { events_buffered: boolean; events: any[] };
                            if (typedEvents.events_buffered && typedEvents.events.length > 0) {
                                console.debug(`[loadSessionTasks] Task ${taskId} has ${typedEvents.events.length} buffered events (batched)`);
                                tasksWithBufferedEvents.set(taskId, typedEvents);
                            }
                        }
                        console.debug(`[loadSessionTasks] Loaded buffered events for ${tasksWithBufferedEvents.size} tasks in single request`);
                    }
                } catch (error) {
                    // Fall back to per-task queries if batched endpoint fails
                    console.warn(`[loadSessionTasks] Batched event fetch failed, falling back to per-task queries:`, error);
                    for (const task of migratedTasks) {
                        try {
                            const response = await api.webui.get(`/api/v1/tasks/${task.taskId}/events/buffered?mark_consumed=false`);
                            if (response.events_buffered && response.events.length > 0) {
                                console.debug(`[loadSessionTasks] Task ${task.taskId} has ${response.events.length} buffered events (fallback)`);
                                tasksWithBufferedEvents.set(task.taskId, response);
                            }
                        } catch (taskError) {
                            console.debug(`[loadSessionTasks] Could not check buffered events for task ${task.taskId}:`, taskError);
                        }
                    }
                }
            }

            for (const task of migratedTasks) {
                const taskMessages = deserializeTaskToMessages(task, sessionId);

                if (tasksWithBufferedEvents.has(task.taskId)) {
                    const userMessages = taskMessages.filter(m => m.isUser);
                    allMessages.push(...userMessages);
                } else {
                    // No buffered events - add all messages from chat_tasks
                    allMessages.push(...taskMessages);
                }
            }

            // Extract feedback state from task metadata
            const feedbackMap: Record<string, { type: "up" | "down"; text: string }> = {};
            // Extract RAG data from task metadata
            const allRagData: RAGSearchResult[] = [];

            for (const task of migratedTasks) {
                if (task.taskMetadata?.feedback) {
                    feedbackMap[task.taskId] = {
                        type: task.taskMetadata.feedback.type,
                        text: task.taskMetadata.feedback.text || "",
                    };
                }

                // Restore RAG data if present
                if (task.taskMetadata?.rag_data && Array.isArray(task.taskMetadata.rag_data)) {
                    allRagData.push(...task.taskMetadata.rag_data);
                }
            }

            // Extract agent name from the most recent task
            // (Use the last task's agent since that's the most recent interaction)
            let agentName: string | null = null;
            for (let i = migratedTasks.length - 1; i >= 0; i--) {
                if (migratedTasks[i].taskMetadata?.agent_name) {
                    agentName = migratedTasks[i].taskMetadata.agent_name;
                    break;
                }
            }

            // Update feedback state
            setSubmittedFeedback(feedbackMap);

            // Restore RAG data
            if (allRagData.length > 0) {
                setRagData(allRagData);
            }

            // Set the agent name if found
            if (agentName) {
                setSelectedAgentName(agentName);
            }

            // Set taskIdInSidePanel to the most recent task for workflow visualization
            if (migratedTasks.length > 0) {
                const mostRecentTask = migratedTasks[migratedTasks.length - 1];
                setTaskIdInSidePanel(mostRecentTask.taskId);
            }

            // Process messages and buffered events to build the final message array
            // We need to reconstruct agent responses from buffered events and insert them
            // in the correct position (after their corresponding user messages)
            if (replayBufferedEventsRef.current && tasksWithBufferedEvents.size > 0) {
                console.debug(`[loadSessionTasks] Processing ${migratedTasks.length} tasks with ${tasksWithBufferedEvents.size} having buffered events`);

                // Build the complete message array with proper ordering
                const finalMessages: MessageFE[] = [];

                // Track tasks where we used buffer reconstruction (need save + cleanup)
                const tasksNeedingSaveAndCleanup: string[] = [];
                // Track tasks where chat_tasks already exists but buffer needs cleanup (cleanup only, no save)
                const tasksNeedingBufferCleanupOnly: string[] = [];

                // Collect tasks that need buffer replay (no saved agent response)
                const tasksNeedingReplay: Array<{ taskId: string; events: any[] }> = [];

                for (const task of migratedTasks) {
                    const taskMessages = deserializeTaskToMessages(task, sessionId);
                    const bufferedData = tasksWithBufferedEvents.get(task.taskId);
                    const agentMessagesFromChatTasks = taskMessages.filter(m => !m.isUser);
                    const userMessages = taskMessages.filter(m => m.isUser);

                    // Check if any agent messages have artifact parts with "in-progress" status
                    // This indicates the artifact was being created when the user switched away
                    // and the buffer may contain completion events we need to replay
                    const hasIncompleteArtifacts = agentMessagesFromChatTasks.some(msg => msg.parts?.some(part => part.kind === "artifact" && (part as ArtifactPart).status === "in-progress"));

                    // IMPORTANT: If chat_tasks already has agent messages, PREFER them over buffer replay.
                    // The buffer should only be used when:
                    // 1. There are no agent messages saved yet (task was running when user switched away)
                    // 2. Buffer replay is needed for incomplete saves
                    // 3. There are incomplete artifacts that need status updates from buffered events
                    // This prevents issues with complex parsing of SSE events.
                    const needsBufferReplay = bufferedData && (agentMessagesFromChatTasks.length === 0 || hasIncompleteArtifacts);

                    if (needsBufferReplay) {
                        // Task has buffered events AND either no saved agent response OR incomplete artifacts - need replay
                        const reason = agentMessagesFromChatTasks.length === 0 ? "no saved agent response" : "incomplete artifacts";
                        console.debug(`[loadSessionTasks] Task ${task.taskId} has buffered events and ${reason}, will replay through handleSseMessage`);
                        // Add user messages first
                        finalMessages.push(...userMessages);
                        // Store for replay after we set initial messages
                        tasksNeedingReplay.push({ taskId: task.taskId, events: bufferedData.events });
                        // Mark this task for save and cleanup (will update session modified date)
                        tasksNeedingSaveAndCleanup.push(task.taskId);
                    } else {
                        // Either no buffered events OR chat_tasks already has the agent response with complete artifacts
                        // Use the saved data from chat_tasks (it has correct ordering)
                        if (bufferedData && agentMessagesFromChatTasks.length > 0) {
                            console.debug(
                                `[loadSessionTasks] Task ${task.taskId} has buffered events but also has ${agentMessagesFromChatTasks.length} saved agent messages with complete artifacts - preferring chat_tasks data, scheduling buffer cleanup only`
                            );
                            // Chat task already exists - only clean up the buffer (don't re-save to avoid updating modified date)
                            tasksNeedingBufferCleanupOnly.push(task.taskId);
                        }
                        finalMessages.push(...taskMessages);
                    }
                }

                // Set all messages in ONE batch to avoid scroll issues
                setMessages(finalMessages);

                // Replay buffered events through handleSseMessage (uses exact same code path as live streaming!)
                // This is done AFTER setting initial messages so user messages are visible first
                if (tasksNeedingReplay.length > 0) {
                    console.debug(`[loadSessionTasks] Replaying ${tasksNeedingReplay.length} tasks through handleSseMessage`);

                    // Use setTimeout to not block the UI update
                    setTimeout(async () => {
                        // Get the current handleSseMessage function via ref
                        const handleSseMessageFn = handleSseMessageRef.current;
                        if (!handleSseMessageFn) {
                            console.error(`[loadSessionTasks] handleSseMessageRef not available for replay`);
                            return;
                        }

                        for (const { taskId, events } of tasksNeedingReplay) {
                            console.debug(`[loadSessionTasks] Replaying ${events.length} events for task ${taskId}`);

                            // Set replay flag to prevent saves during replay (state updates are async)
                            isReplayingEventsRef.current = true;

                            try {
                                // Process each buffered event through handleSseMessage
                                // This is the EXACT same code path as live SSE streaming!
                                for (const bufferedEvent of events) {
                                    const ssePayload = bufferedEvent.data;
                                    if (ssePayload?.data) {
                                        // Create a synthetic MessageEvent-like object
                                        const syntheticEvent = {
                                            data: ssePayload.data,
                                        } as MessageEvent;

                                        // Process through SSE handler - exact same as live streaming
                                        handleSseMessageFn(syntheticEvent);
                                    }
                                }
                            } finally {
                                isReplayingEventsRef.current = false;
                            }
                        }

                        // After replay, get messages and save using setMessages functional updater
                        const taskIdsToSave = tasksNeedingReplay.map(t => t.taskId);
                        setMessages(currentMessages => {
                            setTimeout(async () => {
                                for (const taskId of taskIdsToSave) {
                                    const taskMessages = currentMessages.filter(m => m.taskId === taskId && !m.isStatusBubble);
                                    if (taskMessages.length > 0) {
                                        console.debug(`[loadSessionTasks] Saving task ${taskId} after replay (${taskMessages.length} messages)`);
                                        const messageBubbles = taskMessages.map(serializeMessageBubble);
                                        const userMessage = taskMessages.find(m => m.isUser);
                                        const userMessageText =
                                            userMessage?.parts
                                                ?.filter(p => p.kind === "text")
                                                .map(p => (p as TextPart).text)
                                                .join("") || "";
                                        const hasError = taskMessages.some(m => m.isError);
                                        const taskStatus = hasError ? "error" : "completed";

                                        await saveTaskToBackend(
                                            {
                                                task_id: taskId,
                                                user_message: userMessageText,
                                                message_bubbles: messageBubbles,
                                                task_metadata: {
                                                    schema_version: CURRENT_SCHEMA_VERSION,
                                                    status: taskStatus,
                                                },
                                            },
                                            sessionId
                                        );
                                    } else {
                                        console.warn(`[loadSessionTasks] No messages found for task ${taskId} after replay`);
                                    }
                                }
                            }, 0);
                            // Return unchanged - we're just reading state
                            return currentMessages;
                        });
                    }, 50);
                }

                // Trigger buffer cleanup only (no save) for tasks where chat_tasks already exists
                // This avoids updating the session's modified date just from visiting it
                if (tasksNeedingBufferCleanupOnly.length > 0) {
                    console.debug(`[loadSessionTasks] Scheduling buffer cleanup only for ${tasksNeedingBufferCleanupOnly.length} tasks`);
                    setTimeout(async () => {
                        for (const taskId of tasksNeedingBufferCleanupOnly) {
                            try {
                                await api.webui.delete(`/api/v1/tasks/${taskId}/events/buffered`);
                                console.debug(`[loadSessionTasks] Buffer cleanup successful for task ${taskId}`);
                            } catch (error) {
                                console.error(`[loadSessionTasks] Failed to clean up buffer for task ${taskId}:`, error);
                            }
                        }
                    }, 100);
                }
            } else {
                // No tasks with buffered events - just set all messages at once
                setMessages(allMessages);
            }
        },
        [deserializeTaskToMessages, setRagData, backgroundTasksEnabled, serializeMessageBubble, saveTaskToBackend]
    );

    // Session State
    const [sessionName, setSessionName] = useState<string | null>(null);
    const [sessionToDelete, setSessionToDelete] = useState<Session | null>(null);
    const [isLoadingSession, setIsLoadingSession] = useState<boolean>(false);

    const openSidePanelTab = useCallback((tab: "files" | "activity" | "rag") => {
        setIsSidePanelCollapsed(false);
        setActiveSidePanelTab(tab);

        if (typeof window !== "undefined") {
            window.dispatchEvent(
                new CustomEvent("expand-side-panel", {
                    detail: { tab },
                })
            );
        }
    }, []);

    const closeCurrentEventSource = useCallback(() => {
        if (cancelTimeoutRef.current) {
            clearTimeout(cancelTimeoutRef.current);
            cancelTimeoutRef.current = null;
        }

        if (currentEventSource.current) {
            // Listeners are now removed in the useEffect cleanup
            currentEventSource.current.close();
            currentEventSource.current = null;
        }
        isFinalizing.current = false;
    }, []);

    const handleSseMessage = useCallback(
        (event: MessageEvent) => {
            sseEventSequenceRef.current += 1;
            const currentEventSequence = sseEventSequenceRef.current;
            let rpcResponse: SendStreamingMessageSuccessResponse | JSONRPCErrorResponse;

            try {
                rpcResponse = JSON.parse(event.data) as SendStreamingMessageSuccessResponse | JSONRPCErrorResponse;
            } catch (error: unknown) {
                console.error("Failed to parse SSE message:", error);
                return;
            }

            // Update background task timestamp if this is a background task
            if ("result" in rpcResponse && rpcResponse.result) {
                const result = rpcResponse.result;
                const taskIdFromResult = result.kind === "task" ? result.id : result.kind === "status-update" ? result.taskId : undefined;

                if (taskIdFromResult && isTaskRunningInBackground(taskIdFromResult)) {
                    updateTaskTimestamp(taskIdFromResult, Date.now());
                }
            }

            // Handle RPC Error
            if ("error" in rpcResponse && rpcResponse.error) {
                const errorContent = rpcResponse.error;
                const messageContent = `Error: ${errorContent.message}`;

                setMessages(prev => {
                    const newMessages = prev.filter(msg => !msg.isStatusBubble);
                    newMessages.push({
                        role: "agent",
                        parts: [{ kind: "text", text: messageContent }],
                        isUser: false,
                        isError: true,
                        isComplete: true,
                        metadata: {
                            messageId: `msg-${v4()}`,
                            lastProcessedEventSequence: currentEventSequence,
                        },
                    });
                    return newMessages;
                });

                setIsResponding(false);
                closeCurrentEventSource();
                setCurrentTaskId(null);
                return;
            }

            if (!("result" in rpcResponse) || !rpcResponse.result) {
                console.warn("Received SSE message without a result or error field.", rpcResponse);
                return;
            }

            const result = rpcResponse.result;
            let isFinalEvent = false;
            let messageToProcess: Message | undefined;
            let currentTaskIdFromResult: string | undefined;

            // Determine event type and extract relevant data
            switch (result.kind) {
                case "task":
                    isFinalEvent = true;
                    // For the final task object, extract the error message if the task failed
                    // This handles cases where the task fails before streaming any status updates
                    if (result.status?.state === "failed" && result.status?.message) {
                        messageToProcess = result.status.message;
                    } else {
                        // For successful tasks, content has already been streamed via status_updates
                        messageToProcess = undefined;
                    }
                    currentTaskIdFromResult = result.id;
                    break;
                case "status-update":
                    isFinalEvent = result.final;
                    messageToProcess = result.status?.message;
                    currentTaskIdFromResult = result.taskId;
                    break;
                case "artifact-update":
                    // An artifact was created or updated, refetch the list for the side panel.
                    void artifactsRefetch();
                    return; // No further processing needed for this event.
                default:
                    console.warn("Received unknown result kind in SSE message:", result);
                    return;
            }

            // Process data parts first to extract status text
            if (messageToProcess?.parts) {
                const dataParts = messageToProcess.parts.filter(p => p.kind === "data") as DataPart[];
                if (dataParts.length > 0) {
                    for (const part of dataParts) {
                        const data = part.data as any;
                        if (data && typeof data === "object" && "type" in data) {
                            switch (data.type) {
                                case "agent_progress_update": {
                                    latestStatusText.current = String(data?.status_text ?? "Processing...");
                                    const otherParts = messageToProcess.parts.filter(p => p.kind !== "data");
                                    if (otherParts.length === 0) {
                                        return; // This is a status-only event, do not process further.
                                    }
                                    break;
                                }
                                case "artifact_creation_progress": {
                                    const { filename, status, bytes_transferred, mime_type, description, artifact_chunk, version, rolled_back_text } = data as {
                                        filename: string;
                                        status: "in-progress" | "completed" | "failed" | "cancelled";
                                        bytes_transferred: number;
                                        mime_type?: string;
                                        description?: string;
                                        artifact_chunk?: string;
                                        version?: number;
                                        rolled_back_text?: string;
                                    };

                                    // Handle "cancelled" status - this happens when an artifact block was started
                                    // but never completed (e.g., LLM mentioned artifact syntax in text).
                                    // We remove the in-progress artifact part and add the rolled-back text as a text part.
                                    if (status === "cancelled") {
                                        setMessages(prev => {
                                            const newMessages = [...prev];
                                            const agentMessageIndex = newMessages.findLastIndex(m => !m.isUser && m.taskId === currentTaskIdFromResult);
                                            if (agentMessageIndex !== -1) {
                                                const agentMessage = { ...newMessages[agentMessageIndex], parts: [...newMessages[agentMessageIndex].parts] };
                                                // Remove the artifact part for this filename
                                                agentMessage.parts = agentMessage.parts.filter(p => !(p.kind === "artifact" && p.name === filename));
                                                // Add the rolled-back text as a text part so the user can see it
                                                // This is the original text that was incorrectly parsed as an artifact block
                                                if (rolled_back_text) {
                                                    agentMessage.parts.push({ kind: "text", text: rolled_back_text });
                                                }
                                                newMessages[agentMessageIndex] = agentMessage;
                                            }
                                            return newMessages;
                                        });
                                        // Also remove from artifacts list if it was added
                                        setArtifacts(prevArtifacts => prevArtifacts.filter(a => a.filename !== filename));
                                        return;
                                    }

                                    // Track if we need to trigger auto-download after state update
                                    let shouldAutoDownload = false;

                                    // Update global artifacts list with description and accumulated content
                                    setArtifacts(prevArtifacts => {
                                        const existingIndex = prevArtifacts.findIndex(a => a.filename === filename);
                                        if (existingIndex >= 0) {
                                            // Update existing artifact, preserving description if new one not provided
                                            const updated = [...prevArtifacts];
                                            const existingArtifact = updated[existingIndex];
                                            const isDisplayed = existingArtifact.isDisplayed || false;

                                            // Check if we should trigger auto-download (before state update)
                                            if (status === "completed" && isDisplayed) {
                                                shouldAutoDownload = true;
                                            }

                                            updated[existingIndex] = {
                                                ...existingArtifact,
                                                description: description !== undefined ? description : existingArtifact.description,
                                                size: bytes_transferred || existingArtifact.size,
                                                last_modified: new Date().toISOString(),
                                                // Ensure URI is set
                                                uri: existingArtifact.uri || `artifact://${sessionId}/${filename}`,
                                                // Accumulate content chunks for in-progress and completed artifacts
                                                accumulatedContent:
                                                    status === "in-progress" && artifact_chunk
                                                        ? (existingArtifact.accumulatedContent || "") + artifact_chunk
                                                        : status === "completed" && !isDisplayed
                                                          ? undefined // Clear accumulated content when completed if NOT displayed
                                                          : existingArtifact.accumulatedContent, // Keep for displayed artifacts
                                                // Mark that streaming content is plain text (not base64)
                                                isAccumulatedContentPlainText: status === "in-progress" && artifact_chunk ? true : existingArtifact.isAccumulatedContentPlainText,
                                                // Update mime_type when completed
                                                mime_type: status === "completed" && mime_type ? mime_type : existingArtifact.mime_type,
                                                // Mark that embed resolution is needed when completed
                                                needsEmbedResolution: status === "completed" ? true : existingArtifact.needsEmbedResolution,
                                            };

                                            return updated;
                                        } else {
                                            // Create new artifact entry only if we have description or it's the first chunk
                                            if (description !== undefined || status === "in-progress") {
                                                return [
                                                    ...prevArtifacts,
                                                    {
                                                        filename,
                                                        description: description || null,
                                                        mime_type: mime_type || "application/octet-stream",
                                                        size: bytes_transferred || 0,
                                                        last_modified: new Date().toISOString(),
                                                        uri: `artifact://${sessionId}/${filename}`,
                                                        accumulatedContent: status === "in-progress" && artifact_chunk ? artifact_chunk : undefined,
                                                        isAccumulatedContentPlainText: status === "in-progress" && artifact_chunk ? true : false,
                                                        needsEmbedResolution: status === "completed" ? true : false,
                                                    },
                                                ];
                                            }
                                        }
                                        return prevArtifacts;
                                    });

                                    // Trigger auto-download AFTER state update (outside the setter)
                                    if (shouldAutoDownload) {
                                        setTimeout(() => {
                                            downloadAndResolveArtifact(filename).catch(err => {
                                                console.error(`Auto-download failed for ${filename}:`, err);
                                            });
                                        }, 100);
                                    }

                                    setMessages(prev => {
                                        const newMessages = [...prev];
                                        let agentMessageIndex = newMessages.findLastIndex(m => !m.isUser && m.taskId === currentTaskIdFromResult);

                                        if (agentMessageIndex === -1) {
                                            const newAgentMessage: MessageFE = {
                                                role: "agent",
                                                parts: [],
                                                taskId: currentTaskIdFromResult,
                                                isUser: false,
                                                isComplete: false,
                                                isStatusBubble: false,
                                                metadata: { lastProcessedEventSequence: currentEventSequence },
                                            };
                                            newMessages.push(newAgentMessage);
                                            agentMessageIndex = newMessages.length - 1;
                                        }

                                        const agentMessage = { ...newMessages[agentMessageIndex], parts: [...newMessages[agentMessageIndex].parts] };
                                        agentMessage.isStatusBubble = false;
                                        const artifactPartIndex = agentMessage.parts.findIndex(p => p.kind === "artifact" && p.name === filename);

                                        if (status === "in-progress") {
                                            if (artifactPartIndex > -1) {
                                                const existingPart = agentMessage.parts[artifactPartIndex] as ArtifactPart;
                                                // Create a new part object with immutable update
                                                const updatedPart: ArtifactPart = {
                                                    ...existingPart,
                                                    bytesTransferred: bytes_transferred,
                                                    status: "in-progress",
                                                };
                                                agentMessage.parts[artifactPartIndex] = updatedPart;
                                            } else {
                                                const newPart: ArtifactPart = {
                                                    kind: "artifact",
                                                    status: "in-progress",
                                                    name: filename,
                                                    bytesTransferred: bytes_transferred,
                                                };
                                                agentMessage.parts.push(newPart);
                                            }
                                        } else if (status === "completed") {
                                            const fileAttachment: FileAttachment = {
                                                name: filename,
                                                mime_type,
                                                uri: version !== undefined ? `artifact://${sessionId}/${filename}?version=${version}` : `artifact://${sessionId}/${filename}`,
                                            };
                                            if (artifactPartIndex > -1) {
                                                const existingPart = agentMessage.parts[artifactPartIndex] as ArtifactPart;
                                                // Create a new part object with immutable update
                                                const updatedPart: ArtifactPart = {
                                                    ...existingPart,
                                                    status: "completed",
                                                    file: fileAttachment,
                                                };
                                                // Remove bytesTransferred for completed artifacts
                                                delete updatedPart.bytesTransferred;
                                                agentMessage.parts[artifactPartIndex] = updatedPart;
                                            } else {
                                                agentMessage.parts.push({
                                                    kind: "artifact",
                                                    status: "completed",
                                                    name: filename,
                                                    file: fileAttachment,
                                                });
                                            }
                                            void artifactsRefetch();
                                        } else {
                                            // status === "failed"
                                            const errorMsg = `Failed to create artifact: ${filename}`;
                                            if (artifactPartIndex > -1) {
                                                const existingPart = agentMessage.parts[artifactPartIndex] as ArtifactPart;
                                                // Create a new part object with immutable update
                                                const updatedPart: ArtifactPart = {
                                                    ...existingPart,
                                                    status: "failed",
                                                    error: errorMsg,
                                                };
                                                // Remove bytesTransferred for failed artifacts
                                                delete updatedPart.bytesTransferred;
                                                agentMessage.parts[artifactPartIndex] = updatedPart;
                                            } else {
                                                agentMessage.parts.push({
                                                    kind: "artifact",
                                                    status: "failed",
                                                    name: filename,
                                                    error: errorMsg,
                                                });
                                            }
                                            agentMessage.isError = true;
                                        }

                                        newMessages[agentMessageIndex] = agentMessage;

                                        // Filter out OTHER generic status bubbles, but keep our message.
                                        const finalMessages = newMessages.filter(m => !m.isStatusBubble || m.parts.some(p => p.kind === "artifact" || p.kind === "file"));
                                        return finalMessages;
                                    });
                                    // Return immediately to prevent the generic status handler from running
                                    return;
                                }
                                case "tool_invocation_start":
                                    break;
                                case "authentication_required": {
                                    const auth_uri = data?.auth_uri;
                                    const target_agent = typeof data?.target_agent === "string" ? data.target_agent : "Agent";
                                    const gateway_task_id = typeof data?.gateway_task_id === "string" ? data.gateway_task_id : undefined;
                                    if (typeof auth_uri === "string" && auth_uri.startsWith("http")) {
                                        const authMessage: MessageFE = {
                                            role: "agent",
                                            parts: [{ kind: "text", text: "" }],
                                            authenticationLink: {
                                                url: auth_uri,
                                                text: "Click to Authenticate",
                                                targetAgent: target_agent,
                                                gatewayTaskId: gateway_task_id,
                                            },
                                            isUser: false,
                                            isComplete: true,
                                            metadata: { messageId: `auth-${v4()}` },
                                        };
                                        setMessages(prev => [...prev, authMessage]);
                                    }
                                    break;
                                }
                                case "rag_info_update": {
                                    // Handle RAG info updates from deep research (title and sources)
                                    // This is sent early during research so UI can display title and sources
                                    // Each rag_info_update represents a NEW query with its sources - we create separate entries
                                    // to maintain the query→sources relationship for the timeline display
                                    const ragInfoData = data as {
                                        type: "rag_info_update";
                                        title: string;
                                        query: string;
                                        search_type: string;
                                        search_turn?: number;
                                        sources: Array<{
                                            url: string;
                                            title: string;
                                            favicon?: string;
                                            source_type?: string;
                                            citationId?: string;
                                        }>;
                                        is_complete: boolean;
                                        timestamp: string;
                                    };

                                    if (ragEnabled) {
                                        setRagData(prev => {
                                            // Determine the search turn for citation IDs
                                            // If backend provides search_turn, use it; otherwise count existing entries for this task
                                            const searchTurn = ragInfoData.search_turn ?? prev.filter(r => r.taskId === currentTaskIdFromResult).length;

                                            // Convert sources to RAGSearchResult format
                                            // Use new citation format: s{turn}r{index} (e.g., s0r0, s0r1, s1r0, s1r1)
                                            const formattedSources = (ragInfoData.sources || []).map((source, idx) => ({
                                                // Use backend-provided citationId if available, otherwise generate new format
                                                citationId: source.citationId || `s${searchTurn}r${idx}`,
                                                title: source.title || source.url,
                                                sourceUrl: source.url,
                                                url: source.url,
                                                contentPreview: `Source: ${source.title || source.url}`,
                                                relevanceScore: 1.0,
                                                retrievedAt: ragInfoData.timestamp,
                                                metadata: {
                                                    favicon: source.favicon || `https://www.google.com/s2/favicons?domain=${source.url}&sz=32`,
                                                    type: "web_search",
                                                    sourceType: source.source_type || "web",
                                                },
                                            }));

                                            // Find existing entry for this SPECIFIC query within this task
                                            // This allows multiple queries per task, each with their own sources
                                            const existingIndex = prev.findIndex(r => r.searchType === "deep_research" && r.taskId === currentTaskIdFromResult && r.query === ragInfoData.query);

                                            if (existingIndex !== -1) {
                                                // Update existing entry for this query - merge sources (avoid duplicates)
                                                const updated = [...prev];
                                                const existing = updated[existingIndex];

                                                const existingUrls = new Set(existing.sources.map(s => s.sourceUrl || s.url));
                                                const newSources = formattedSources.filter(s => !existingUrls.has(s.sourceUrl || s.url));

                                                updated[existingIndex] = {
                                                    ...existing,
                                                    title: ragInfoData.title || existing.title, // Update title if provided
                                                    sources: [...existing.sources, ...newSources],
                                                };

                                                return updated;
                                            } else {
                                                // Create a NEW entry for this query
                                                // This maintains separate query→sources relationships for the timeline
                                                const newEntry: RAGSearchResult = {
                                                    query: ragInfoData.query,
                                                    title: ragInfoData.title, // Use LLM-generated title
                                                    searchType: "deep_research" as const,
                                                    timestamp: ragInfoData.timestamp,
                                                    sources: formattedSources,
                                                    taskId: currentTaskIdFromResult,
                                                };
                                                return [...prev, newEntry];
                                            }
                                        });
                                    }
                                    break;
                                }
                                case "deep_research_progress": {
                                    // Deep research progress tracking - keep the data part for ChatMessage to render
                                    // Clear latestStatusText so LoadingMessageRow doesn't show duplicate status
                                    latestStatusText.current = null;

                                    // Extract progress data to build query history
                                    const progressData = data as {
                                        type: "deep_research_progress";
                                        phase: string;
                                        current_query: string;
                                        fetching_urls: Array<{ url: string; title: string; favicon: string; source_type?: string }>;
                                    };

                                    if (currentTaskIdFromResult) {
                                        const taskHistory = deepResearchQueryHistoryRef.current.get(currentTaskIdFromResult) || [];

                                        if (progressData.current_query) {
                                            // We have a query - either add it or update its URLs
                                            const existingQueryIndex = taskHistory.findIndex(q => q.query === progressData.current_query);

                                            if (existingQueryIndex === -1) {
                                                // New query - add it (URLs may come later in analyzing phase)
                                                taskHistory.push({
                                                    query: progressData.current_query,
                                                    timestamp: new Date().toISOString(),
                                                    urls: progressData.fetching_urls || [],
                                                });
                                            } else if (progressData.fetching_urls && progressData.fetching_urls.length > 0) {
                                                // Existing query AND we have URLs - update/merge URLs
                                                const existingEntry = taskHistory[existingQueryIndex];
                                                const existingUrls = new Set(existingEntry.urls.map(u => u.url));
                                                const newUrls = progressData.fetching_urls.filter(u => !existingUrls.has(u.url));
                                                if (newUrls.length > 0) {
                                                    taskHistory[existingQueryIndex] = {
                                                        ...existingEntry,
                                                        urls: [...existingEntry.urls, ...newUrls],
                                                    };
                                                }
                                            }
                                        } else if (progressData.fetching_urls && progressData.fetching_urls.length > 0 && taskHistory.length > 0) {
                                            // No current_query but we have URLs - update the LAST query's URLs
                                            // This happens during "analyzing" phase when backend doesn't send current_query
                                            const lastQueryIndex = taskHistory.length - 1;
                                            const lastEntry = taskHistory[lastQueryIndex];
                                            const existingUrls = new Set(lastEntry.urls.map(u => u.url));
                                            const newUrls = progressData.fetching_urls.filter(u => !existingUrls.has(u.url));
                                            if (newUrls.length > 0) {
                                                taskHistory[lastQueryIndex] = {
                                                    ...lastEntry,
                                                    urls: [...lastEntry.urls, ...newUrls],
                                                };
                                            }
                                        }

                                        deepResearchQueryHistoryRef.current.set(currentTaskIdFromResult, taskHistory);

                                        // ALWAYS inject query_history so the component can display accumulated progress
                                        // Even if we didn't modify the history, we need to pass it to the component
                                        (data as any).query_history = taskHistory;
                                    }

                                    // Don't return early - let the data part flow through to the message
                                    break;
                                }
                                case "tool_result": {
                                    // Handle tool results that may contain RAG metadata
                                    const resultData = (data as any).result_data;

                                    // Check if result_data contains rag_metadata
                                    if (resultData && typeof resultData === "object" && resultData.rag_metadata) {
                                        const ragMetadata = resultData.rag_metadata;
                                        if (ragMetadata && ragEnabled) {
                                            console.log("[ChatProvider] Received tool_result with RAG metadata:", {
                                                searchType: ragMetadata.searchType,
                                                sourcesCount: ragMetadata.sources?.length,
                                                taskId: currentTaskIdFromResult,
                                                sampleSources: ragMetadata.sources?.slice(0, 3).map((s: any) => ({
                                                    title: s.title,
                                                    fetched: s.metadata?.fetched,
                                                    fetch_status: s.metadata?.fetch_status,
                                                    contentPreview: s.contentPreview?.substring(0, 100),
                                                })),
                                            });

                                            const ragSearchResult: RAGSearchResult = {
                                                query: ragMetadata.query,
                                                title: ragMetadata.title,
                                                searchType: ragMetadata.searchType,
                                                timestamp: ragMetadata.timestamp,
                                                sources: ragMetadata.sources,
                                                taskId: currentTaskIdFromResult,
                                                metadata: ragMetadata.metadata, // Preserve metadata with query breakdown
                                            };

                                            // For deep research: REPLACE all previous entries for this task with the final metadata
                                            // This ensures we have the complete, properly structured data with metadata.queries
                                            if (ragMetadata.searchType === "deep_research") {
                                                console.log("[ChatProvider] Replacing deep research RAG data for task", currentTaskIdFromResult);
                                                setRagData(prev => {
                                                    const beforeCount = prev.filter(r => r.searchType === "deep_research" && r.taskId === currentTaskIdFromResult).length;
                                                    // Remove all previous deep research entries for this task
                                                    const filtered = prev.filter(r => !(r.searchType === "deep_research" && r.taskId === currentTaskIdFromResult));
                                                    // Add the final complete entry
                                                    const result = [...filtered, ragSearchResult];
                                                    console.log("[ChatProvider] Deep research RAG data replaced:", {
                                                        removedEntries: beforeCount,
                                                        newSourcesCount: ragSearchResult.sources.length,
                                                    });
                                                    return result;
                                                });
                                            } else {
                                                // For regular web search: append as before
                                                setRagData(prev => [...prev, ragSearchResult]);
                                            }
                                        }
                                    }
                                    // Don't add tool_result to content parts - it's metadata only
                                    break;
                                }
                                default:
                                    console.log("Received unknown data part type:", data.type);
                            }
                        }
                    }
                }
            }

            // Filter out data parts EXCEPT deep_research_progress which needs to be rendered
            // Also filter out text parts when we have deep_research_progress to show progress-only
            const hasDeepResearchProgress = messageToProcess?.parts?.some(p => {
                if (p.kind === "data") {
                    const dataPart = p as DataPart;
                    return dataPart.data && (dataPart.data as any).type === "deep_research_progress";
                }
                return false;
            });

            const newContentParts =
                messageToProcess?.parts?.filter(p => {
                    // Keep deep_research_progress data parts
                    if (p.kind === "data") {
                        const dataPart = p as DataPart;
                        return dataPart.data && (dataPart.data as any).type === "deep_research_progress";
                    }
                    // Filter out text parts if we have deep research progress (to show progress-only)
                    if (p.kind === "text" && hasDeepResearchProgress) {
                        return false;
                    }
                    // Keep files and artifacts
                    return true;
                }) || [];
            const hasNewFiles = newContentParts.some(p => p.kind === "file");

            // Check if this is a failed task
            const isTaskFailed = result.kind === "task" && result.status?.state === "failed";

            // Update UI state based on processed parts
            setMessages(prevMessages => {
                const newMessages = [...prevMessages];

                let lastMessage = newMessages[newMessages.length - 1];

                // Remove old generic status bubble
                if (lastMessage?.isStatusBubble) {
                    newMessages.pop();
                    lastMessage = newMessages[newMessages.length - 1];
                }

                // Check if this is a deep research progress update
                const isProgressUpdate = newContentParts.length === 1 && newContentParts[0].kind === "data" && (newContentParts[0] as DataPart).data && ((newContentParts[0] as DataPart).data as any).type === "deep_research_progress";

                // For progress updates, always update the same message (don't replace, just update the data)
                // The InlineResearchProgress component will handle showing all stages
                if (isProgressUpdate && lastMessage && !lastMessage.isUser && lastMessage.taskId === (result as TaskStatusUpdateEvent).taskId) {
                    const updatedMessage: MessageFE = {
                        ...lastMessage,
                        parts: newContentParts,
                        isComplete: isFinalEvent || hasNewFiles,
                        metadata: {
                            ...lastMessage.metadata,
                            lastProcessedEventSequence: currentEventSequence,
                        },
                    };
                    newMessages[newMessages.length - 1] = updatedMessage;
                } else if (lastMessage && !lastMessage.isUser && lastMessage.taskId === (result as TaskStatusUpdateEvent).taskId && newContentParts.length > 0) {
                    // Regular append for non-progress updates
                    const updatedMessage: MessageFE = {
                        ...lastMessage,
                        parts: [...lastMessage.parts, ...newContentParts],
                        isComplete: isFinalEvent || hasNewFiles,
                        isError: isTaskFailed || lastMessage.isError,
                        metadata: {
                            ...lastMessage.metadata,
                            lastProcessedEventSequence: currentEventSequence,
                        },
                    };
                    newMessages[newMessages.length - 1] = updatedMessage;
                } else {
                    // For failed tasks, always create a message bubble even if there are no content parts
                    // For other cases, only create a new bubble if there is visible content to render.
                    // Include deep_research_progress data parts as visible content
                    const hasVisibleContent =
                        isTaskFailed || newContentParts.some(p => (p.kind === "text" && (p as TextPart).text.trim()) || p.kind === "file" || (p.kind === "data" && (p as DataPart).data && (p as DataPart).data.type === "deep_research_progress"));
                    if (hasVisibleContent) {
                        const newBubble: MessageFE = {
                            role: "agent",
                            parts: newContentParts,
                            taskId: currentTaskIdFromResult,
                            isUser: false,
                            isComplete: isFinalEvent || hasNewFiles,
                            isError: isTaskFailed,
                            metadata: {
                                messageId: rpcResponse.id?.toString() || `msg-${v4()}`,
                                sessionId: (result as TaskStatusUpdateEvent).contextId,
                                lastProcessedEventSequence: currentEventSequence,
                            },
                        };
                        newMessages.push(newBubble);
                    }
                }

                // Add a new status bubble if the task is not over
                if (isFinalEvent) {
                    latestStatusText.current = null;
                    // Finalize any lingering in-progress artifact parts for this task
                    // With the new artifact_completed signal, in-progress artifacts should only remain if they truly failed
                    for (let i = newMessages.length - 1; i >= 0; i--) {
                        const msg = newMessages[i];
                        if (msg.taskId === currentTaskIdFromResult && msg.parts.some(p => p.kind === "artifact" && p.status === "in-progress")) {
                            const finalParts: PartFE[] = msg.parts.map(p => {
                                if (p.kind === "artifact" && p.status === "in-progress") {
                                    // Mark in-progress artifact as failed since artifact_completed signal should have arrived
                                    return { ...p, status: "failed", error: `Artifact creation for "${p.name}" did not complete.` };
                                }
                                return p;
                            });

                            newMessages[i] = {
                                ...msg,
                                parts: finalParts,
                                isError: true, // Mark as error because artifacts failed
                                isComplete: true,
                            };
                        }
                    }
                    // Explicitly mark the last message as complete on the final event
                    const taskMessageIndex = newMessages.findLastIndex(msg => !msg.isUser && msg.taskId === currentTaskIdFromResult);

                    if (taskMessageIndex !== -1) {
                        newMessages[taskMessageIndex] = {
                            ...newMessages[taskMessageIndex],
                            isComplete: true,
                            metadata: { ...newMessages[taskMessageIndex].metadata, lastProcessedEventSequence: currentEventSequence },
                        };
                    } else if (result.kind === "task" && result.status?.state !== "failed" && result.status?.message?.parts) {
                        // Fallback: the final response arrived before any status updates
                        // (race condition between response and status broker topics).
                        // Create a bubble from the final response's content.
                        const fallbackParts = (result.status.message.parts as PartFE[]).filter((p: PartFE) => p.kind === "text" || p.kind === "file");
                        if (fallbackParts.length > 0) {
                            newMessages.push({
                                role: "agent",
                                parts: fallbackParts,
                                taskId: currentTaskIdFromResult,
                                isUser: false,
                                isComplete: true,
                                metadata: {
                                    messageId: rpcResponse.id?.toString() || `msg-${v4()}`,
                                    sessionId: (result as unknown as { contextId?: string }).contextId || sessionId,
                                    lastProcessedEventSequence: currentEventSequence,
                                },
                            });
                        }
                    }
                }

                // Synchronously update messagesRef so other code reads correct state
                messagesRef.current = newMessages;

                // Save task on final event directly from inside this callback.
                if (isFinalEvent && currentTaskIdFromResult && !isReplayingEventsRef.current) {
                    const taskMessagesToSave = newMessages.filter(msg => msg.taskId === currentTaskIdFromResult && !msg.isStatusBubble);
                    if (taskMessagesToSave.length > 0) {
                        const isBackgroundTask = isTaskRunningInBackground(currentTaskIdFromResult);
                        const taskSessionId = (result as TaskStatusUpdateEvent).contextId || sessionId;

                        // Serialize all message bubbles
                        const messageBubbles = taskMessagesToSave.map(serializeMessageBubble);

                        // Extract user message text
                        const userMessage = taskMessagesToSave.find(m => m.isUser);
                        const userMessageText =
                            userMessage?.parts
                                ?.filter(p => p.kind === "text")
                                .map(p => (p as TextPart).text)
                                .join("") || "";

                        // Determine task status
                        const hasError = taskMessagesToSave.some(m => m.isError);
                        const taskStatus = hasError ? "error" : "completed";

                        const taskRagData = ragDataRef.current.filter(r => r.taskId === currentTaskIdFromResult);

                        // Save complete task (async - fires fetch, returns Promise immediately)
                        saveTaskToBackend(
                            {
                                task_id: currentTaskIdFromResult,
                                user_message: userMessageText,
                                message_bubbles: messageBubbles,
                                task_metadata: {
                                    schema_version: CURRENT_SCHEMA_VERSION,
                                    status: taskStatus,
                                    agent_name: selectedAgentName,
                                    rag_data: taskRagData.length > 0 ? taskRagData : undefined,
                                },
                            },
                            taskSessionId
                        )
                            .then(async saved => {
                                if (saved) {
                                    if (typeof window !== "undefined") {
                                        window.dispatchEvent(new CustomEvent("new-chat-session"));
                                    }
                                }

                                // Unregister background task after save completes
                                if (isBackgroundTask) {
                                    unregisterBackgroundTask(currentTaskIdFromResult);
                                }

                                // Handle session title based on feature flag
                                if (taskSessionId && !sessionsWithAutoGeneratedTitles.current.has(taskSessionId)) {
                                    if (autoTitleGenerationEnabled) {
                                        const agentMessage = taskMessagesToSave.find(m => !m.isUser);
                                        const agentResponseText =
                                            agentMessage?.parts
                                                ?.filter(p => p.kind === "text")
                                                .map(p => (p as TextPart).text)
                                                .join("") || "";

                                        if (userMessageText && agentResponseText) {
                                            sessionsWithAutoGeneratedTitles.current.add(taskSessionId);
                                            generateTitle(taskSessionId, userMessageText, agentResponseText).catch(error => {
                                                console.error("[ChatProvider] Title generation failed:", error);
                                            });
                                        }
                                    }
                                }
                            })
                            .catch(error => {
                                console.error(`[ChatProvider] Error saving task ${currentTaskIdFromResult}:`, error);
                                if (isBackgroundTask) {
                                    unregisterBackgroundTask(currentTaskIdFromResult);
                                }
                            });
                    } else if (isTaskRunningInBackground(currentTaskIdFromResult)) {
                        // No messages but task was background - still unregister
                        unregisterBackgroundTask(currentTaskIdFromResult);
                        if (typeof window !== "undefined") {
                            window.dispatchEvent(new CustomEvent("new-chat-session"));
                        }
                    }
                }

                return newMessages;
            });

            // Finalization logic (non-save operations only — save is inside setMessages above)
            if (isFinalEvent) {
                if (isCancellingRef.current) {
                    addNotification("Task cancelled.", "success");
                    if (cancelTimeoutRef.current) clearTimeout(cancelTimeoutRef.current);
                    setIsCancelling(false);
                }

                // Mark all in-progress artifacts as completed when task finishes
                setMessages(currentMessages => {
                    return currentMessages.map(msg => {
                        if (msg.isUser) return msg;

                        const hasInProgressArtifacts = msg.parts.some(p => p.kind === "artifact" && (p as ArtifactPart).status === "in-progress");

                        if (!hasInProgressArtifacts) return msg;

                        return {
                            ...msg,
                            parts: msg.parts.map(part => {
                                if (part.kind === "artifact" && (part as ArtifactPart).status === "in-progress") {
                                    const artifactPart = part as ArtifactPart;
                                    const fileAttachment: FileAttachment = {
                                        name: artifactPart.name,
                                        mime_type: artifactPart.file?.mime_type,
                                        uri: `artifact://${sessionId}/${artifactPart.name}`,
                                    };
                                    const completedPart: ArtifactPart = {
                                        kind: "artifact",
                                        status: "completed",
                                        name: artifactPart.name,
                                        file: fileAttachment,
                                    };
                                    return completedPart;
                                }
                                return part;
                            }),
                        };
                    });
                });

                // Background task unregistration is now handled in the saveTaskToBackend promise above
                // This ensures the database save completes before we unregister and refresh

                setIsResponding(false);
                closeCurrentEventSource();
                setCurrentTaskId(null);
                isFinalizing.current = true;
                void artifactsRefetch();

                // Clean up query history for this completed task
                if (currentTaskIdFromResult) {
                    deepResearchQueryHistoryRef.current.delete(currentTaskIdFromResult);
                }

                setTimeout(() => {
                    isFinalizing.current = false;
                }, 100);
            }
        },
        // eslint-disable-next-line react-hooks/exhaustive-deps
        [
            addNotification,
            closeCurrentEventSource,
            artifactsRefetch,
            sessionId,
            selectedAgentName,
            saveTaskToBackend,
            serializeMessageBubble,
            downloadAndResolveArtifact,
            setArtifacts,
            setRagData,
            ragEnabled,
            isTaskRunningInBackground,
            updateTaskTimestamp,
            unregisterBackgroundTask,
            generateTitle,
        ]
    );

    // Helper function to replay buffered SSE events for a background task
    // This is used when a background task completed while the user was away
    const replayBufferedEvents = useCallback(
        async (taskId: string): Promise<boolean> => {
            try {
                console.debug(`[ChatProvider] Fetching buffered events for task ${taskId}`);
                const response = await api.webui.get(`/api/v1/tasks/${taskId}/events/buffered`);

                if (!response.events_buffered || response.events.length === 0) {
                    console.debug(`[ChatProvider] No buffered events for task ${taskId}`);
                    return false;
                }

                console.debug(`[ChatProvider] Replaying ${response.events.length} buffered events for task ${taskId}`);

                // Reset the SSE event sequence counter for replay
                // This ensures proper sequencing during replay
                const originalSequence = sseEventSequenceRef.current;
                sseEventSequenceRef.current = 0;

                // Clear any existing AGENT messages for this task before replaying
                // Keep user messages since they're not in the SSE events
                // This prevents duplicate content when replaying while preserving user input
                setMessages(prev => prev.filter(msg => msg.taskId !== taskId || msg.isUser));

                // Set the replay flag to prevent save operations during replay
                // The data is already persisted, so we don't want to re-save and update timestamps
                isReplayingEventsRef.current = true;

                try {
                    // Process each buffered event through the SSE handler
                    for (const bufferedEvent of response.events) {
                        // The buffered event data contains the full SSE payload
                        // which has {event: "message", data: "...serialized JSON..."}
                        const ssePayload = bufferedEvent.data;

                        if (ssePayload?.data) {
                            // Create a synthetic MessageEvent-like object
                            const syntheticEvent = {
                                data: ssePayload.data, // This is the serialized JSON string
                            } as MessageEvent;

                            // Process through the SSE handler
                            handleSseMessage(syntheticEvent);
                        }
                    }
                } finally {
                    // Always reset the replay flag, even if an error occurred
                    isReplayingEventsRef.current = false;
                }

                // Restore the sequence counter (or keep the new value if higher)
                sseEventSequenceRef.current = Math.max(originalSequence, sseEventSequenceRef.current);

                console.debug(`[ChatProvider] Finished replaying buffered events for task ${taskId}`);
                return true;
            } catch (error) {
                console.error(`[ChatProvider] Error replaying buffered events for task ${taskId}:`, error);
                isReplayingEventsRef.current = false; // Reset flag on error
                return false;
            }
        },
        [handleSseMessage]
    );

    // Keep the ref in sync with the latest replayBufferedEvents function
    useEffect(() => {
        replayBufferedEventsRef.current = replayBufferedEvents;
    }, [replayBufferedEvents]);

    // Core implementation - called directly or after confirmation
    const handleNewSessionCore = useCallback(
        async (preserveProjectContext: boolean = false) => {
            const log_prefix = "ChatProvider.handleNewSession:";

            closeCurrentEventSource();

            if (isResponding && currentTaskId && selectedAgentName && !isCancelling) {
                const isBackground = isTaskRunningInBackground(currentTaskId);
                if (!isBackground) {
                    api.webui
                        .post(`/api/v1/tasks/${currentTaskId}:cancel`, {
                            jsonrpc: "2.0",
                            id: `req-${v4()}`,
                            method: "tasks/cancel",
                            params: { id: currentTaskId },
                        })
                        .catch(error => console.warn(`${log_prefix} Failed to cancel current task:`, error));
                }
            }

            if (cancelTimeoutRef.current) {
                clearTimeout(cancelTimeoutRef.current);
                cancelTimeoutRef.current = null;
            }
            setIsCancelling(false);

            // Clear session ID - will be set by backend when first message is sent
            setSessionId("");

            // Clear session name - will be set when first message is sent
            setSessionName(null);

            // Clear project context when starting a new chat outside of a project
            if (activeProject && !preserveProjectContext) {
                setActiveProject(null);
            } else if (activeProject && preserveProjectContext) {
                console.log(`${log_prefix} Preserving project context: ${activeProject.name}`);
            }

            setSelectedAgentName("");
            setMessages([]);
            setIsResponding(false);
            setCurrentTaskId(null);
            setTaskIdInSidePanel(null);
            closePreview();
            isFinalizing.current = false;
            latestStatusText.current = null;
            sseEventSequenceRef.current = 0;
            // Clear RAG data on new session
            setRagData([]);
            // Clear deep research query history
            deepResearchQueryHistoryRef.current.clear();
            // Artifacts will be automatically refreshed by useArtifacts hook when sessionId changes

            // Dispatch event to focus chat input
            if (typeof window !== "undefined") {
                window.dispatchEvent(new CustomEvent("focus-chat-input"));
            }

            // Note: No session events dispatched here since no session exists yet.
            // Session creation event will be dispatched when first message creates the actual session.
        },
        [isResponding, currentTaskId, selectedAgentName, isCancelling, closeCurrentEventSource, activeProject, setActiveProject, closePreview, isTaskRunningInBackground, setRagData]
    );

    // Wrapper that shows confirmation when task is running and background tasks are disabled
    const handleNewSession = useCallback(
        async (preserveProjectContext: boolean = false) => {
            // Check if we need to warn the user about losing a running task
            if (isResponding && backgroundTasksEnabled === false) {
                // Store the action to execute after confirmation
                setPendingNavigationAction(() => () => handleNewSessionCore(preserveProjectContext));
                setRunningTaskWarningOpen(true);
                return;
            }
            // No warning needed - proceed directly
            await handleNewSessionCore(preserveProjectContext);
        },
        [isResponding, backgroundTasksEnabled, handleNewSessionCore]
    );

    // Start a new chat session with a prompt template pre-filled
    const startNewChatWithPrompt = useCallback(
        (promptData: PendingPromptData) => {
            // Store the pending prompt - it will be applied after the session is ready
            setPendingPrompt(promptData);
            // Start a new session
            handleNewSession();
        },
        [handleNewSession]
    );

    // Clear the pending prompt (called after it's been applied)
    const clearPendingPrompt = useCallback(() => {
        setPendingPrompt(null);
    }, []);

    // Core implementation - called directly or after confirmation
    const handleSwitchSessionCore = useCallback(
        async (newSessionId: string) => {
            const log_prefix = "ChatProvider.handleSwitchSession:";
            console.log(`${log_prefix} Switching to session ${newSessionId}...`);

            setIsLoadingSession(true);

            // Check if we're switching away from a session with a running background task
            const currentSessionBackgroundTasks = backgroundTasks.filter(t => t.sessionId === sessionId);
            const hasRunningBackgroundTask = currentSessionBackgroundTasks.some(t => t.taskId === currentTaskId);

            // DON'T clear messages if there are background tasks in the current session
            // This ensures the messages are available for saving when the task completes
            const hasAnyBackgroundTasks = currentSessionBackgroundTasks.length > 0;

            if (!hasRunningBackgroundTask && !hasAnyBackgroundTasks) {
                setMessages([]);
            }

            closeCurrentEventSource();

            if (isResponding && currentTaskId && selectedAgentName && !isCancelling) {
                const isBackground = isTaskRunningInBackground(currentTaskId);
                if (!isBackground) {
                    console.log(`${log_prefix} Cancelling current task ${currentTaskId}`);
                    try {
                        await api.webui.post(`/api/v1/tasks/${currentTaskId}:cancel`, {
                            jsonrpc: "2.0",
                            id: `req-${v4()}`,
                            method: "tasks/cancel",
                            params: { id: currentTaskId },
                        });
                    } catch (error) {
                        console.warn(`${log_prefix} Failed to cancel current task:`, error);
                    }
                }
            }

            if (cancelTimeoutRef.current) {
                clearTimeout(cancelTimeoutRef.current);
                cancelTimeoutRef.current = null;
            }
            setIsCancelling(false);

            try {
                // Load session metadata first to get project info
                const sessionData = await api.webui.get(`/api/v1/sessions/${newSessionId}`);
                const session: Session | null = sessionData?.data;
                setSessionName(session?.name ?? "N/A");

                // Activate or deactivate project context based on session's project
                // Set flag to prevent handleNewSession from being triggered by this project change
                isSessionSwitchRef.current = true;

                if (session?.projectId) {
                    console.log(`${log_prefix} Session belongs to project ${session.projectId}`);

                    // Check if we're already in the correct project context
                    if (activeProject?.id !== session.projectId) {
                        // Find the full project object from the projects array
                        const project = projects.find((p: Project) => p.id === session?.projectId);

                        if (project) {
                            console.log(`${log_prefix} Activating project context: ${project.name}`);
                            setActiveProject(project);
                        } else {
                            console.warn(`${log_prefix} Project ${session.projectId} not found in projects array`);
                        }
                    } else {
                        console.log(`${log_prefix} Already in correct project context`);
                    }
                } else {
                    // Session has no project - deactivate project context
                    if (activeProject !== null) {
                        console.log(`${log_prefix} Session has no project, deactivating project context`);
                        setActiveProject(null);
                    }
                }

                // Update session ID state
                setSessionId(newSessionId);

                // Reset other session-related state
                setIsResponding(false);
                setCurrentTaskId(null);
                setTaskIdInSidePanel(null);
                closePreview();
                isFinalizing.current = false;
                latestStatusText.current = null;
                sseEventSequenceRef.current = 0;
                // Clear RAG data when switching sessions - will be repopulated by loadSessionTasks
                setRagData([]);
                // Clear deep research query history when switching sessions
                deepResearchQueryHistoryRef.current.clear();

                await loadSessionTasks(newSessionId);

                // Check for running background tasks in this session and reconnect
                const sessionBackgroundTasks = backgroundTasks.filter(t => t.sessionId === newSessionId);
                if (sessionBackgroundTasks.length > 0) {
                    // Check if any are still running
                    for (const bgTask of sessionBackgroundTasks) {
                        const status = await checkTaskStatus(bgTask.taskId);
                        if (status && status.is_running) {
                            console.log(`[ChatProvider] Reconnecting to running background task ${bgTask.taskId}`);
                            setCurrentTaskId(bgTask.taskId);
                            setIsResponding(true);
                            if (bgTask.agentName) {
                                setSelectedAgentName(bgTask.agentName);
                            }
                            // Only reconnect to the first running task
                            break;
                        } else {
                            // Task is no longer running - it completed while we were away
                            console.log(`[ChatProvider] Background task ${bgTask.taskId} completed while away, already reconstructed in loadSessionTasks`);

                            // Unregister the background task
                            unregisterBackgroundTask(bgTask.taskId);

                            // Trigger title generation for completed background task (if feature is enabled)
                            // This handles the case where the task completed during browser refresh
                            if (autoTitleGenerationEnabled && newSessionId && !sessionsWithAutoGeneratedTitles.current.has(newSessionId)) {
                                console.log(`[ChatProvider] Triggering title generation for completed background task ${bgTask.taskId} in session ${newSessionId}`);
                                sessionsWithAutoGeneratedTitles.current.add(newSessionId);

                                // Fetch messages from database since they won't be in memory after refresh
                                (async () => {
                                    try {
                                        const taskData = await api.webui.get(`/api/v1/sessions/${newSessionId}/chat-tasks`);
                                        const tasks = taskData.tasks || [];
                                        console.log(`[ChatProvider] Fetched ${tasks.length} tasks from database for title generation`);

                                        // Find the task that matches
                                        const matchingTask = tasks.find((t: StoredTaskData) => t.taskId === bgTask.taskId);
                                        if (matchingTask) {
                                            const messageBubbles = JSON.parse(matchingTask.messageBubbles);
                                            let userMessageText = "";
                                            let agentResponseText = "";

                                            for (const bubble of messageBubbles) {
                                                const text = bubble.text || "";
                                                if (bubble.type === "user" && !userMessageText) {
                                                    userMessageText = text;
                                                } else if (bubble.type === "agent" && !agentResponseText) {
                                                    agentResponseText = text;
                                                }
                                            }

                                            if (userMessageText && agentResponseText) {
                                                console.log(`[ChatProvider] Calling generateTitle for session ${newSessionId} (post-refresh)`);
                                                await generateTitle(newSessionId, userMessageText, agentResponseText);
                                                console.log(`[ChatProvider] Title generation completed for session ${newSessionId}`);
                                            } else {
                                                console.warn(`[ChatProvider] Cannot generate title - missing messages. User: ${!!userMessageText}, Agent: ${!!agentResponseText}`);
                                            }
                                        } else {
                                            console.warn(`[ChatProvider] Task ${bgTask.taskId} not found in database`);
                                        }
                                    } catch (error) {
                                        console.error("[ChatProvider] Error generating title for completed background task:", error);
                                    }
                                })();
                            }
                        }
                    }
                }
            } catch (error) {
                setError({ title: "Switching Chats Failed", error: getErrorMessage(error, "Failed to switch chat sessions.") });
            } finally {
                setIsLoadingSession(false);
            }
        },
        [
            closeCurrentEventSource,
            isResponding,
            currentTaskId,
            selectedAgentName,
            isCancelling,
            loadSessionTasks,
            activeProject,
            projects,
            setActiveProject,
            closePreview,
            setError,
            setRagData,
            backgroundTasks,
            checkTaskStatus,
            sessionId,
            unregisterBackgroundTask,
            autoTitleGenerationEnabled,
            generateTitle,
            isTaskRunningInBackground,
            replayBufferedEvents,
        ]
    );

    // Wrapper that shows confirmation when task is running and background tasks are disabled
    const handleSwitchSession = useCallback(
        async (newSessionId: string) => {
            // Check if we need to warn the user about losing a running task
            if (isResponding && backgroundTasksEnabled === false) {
                // Store the action to execute after confirmation
                setPendingNavigationAction(() => () => handleSwitchSessionCore(newSessionId));
                setRunningTaskWarningOpen(true);
                return;
            }
            // No warning needed - proceed directly
            await handleSwitchSessionCore(newSessionId);
        },
        [isResponding, backgroundTasksEnabled, handleSwitchSessionCore]
    );

    const updateSessionName = useCallback(
        async (sessionId: string, newName: string) => {
            try {
                const response = await api.webui.patch(`/api/v1/sessions/${sessionId}`, { name: newName }, { fullResponse: true });

                if (response.status === 422) {
                    throw new Error("Invalid name");
                }

                if (!response.ok) {
                    const errorData = await response.json().catch(() => ({}));
                    throw new Error(errorData.message || "Failed to update session name");
                }

                setSessionName(newName);
                if (typeof window !== "undefined") {
                    window.dispatchEvent(new CustomEvent("new-chat-session"));
                }
            } catch (error) {
                setError({ title: "Session Name Update Failed", error: getErrorMessage(error, "Failed to update session name.") });
            }
        },
        [setError]
    );

    const deleteSession = useCallback(
        async (sessionIdToDelete: string) => {
            try {
                await api.webui.delete(`/api/v1/sessions/${sessionIdToDelete}`);
                addNotification("Session deleted.", "success");
                if (sessionIdToDelete === sessionId) {
                    handleNewSession();
                }
                // Trigger session list refresh
                if (typeof window !== "undefined") {
                    window.dispatchEvent(new CustomEvent("new-chat-session"));
                }
            } catch (error) {
                setError({ title: "Chat Deletion Failed", error: getErrorMessage(error, "Failed to delete session.") });
            }
        },
        [addNotification, handleNewSession, sessionId, setError]
    );

    // Artifact Display and Cache Management
    const markArtifactAsDisplayed = useCallback((filename: string, displayed: boolean) => {
        setArtifacts(prevArtifacts => {
            return prevArtifacts.map(artifact => (artifact.filename === filename ? { ...artifact, isDisplayed: displayed } : artifact));
        });
        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []); // setArtifacts is stable from useState

    const openSessionDeleteModal = useCallback((session: Session) => {
        setSessionToDelete(session);
    }, []);

    const closeSessionDeleteModal = useCallback(() => {
        setSessionToDelete(null);
    }, []);

    const confirmSessionDelete = useCallback(async () => {
        if (sessionToDelete) {
            await deleteSession(sessionToDelete.id);
            setSessionToDelete(null);
        }
    }, [sessionToDelete, deleteSession]);

    const handleCancel = useCallback(async () => {
        if ((!isResponding && !isCancelling) || !currentTaskId) {
            return;
        }
        if (isCancelling) {
            return;
        }

        setIsCancelling(true);

        try {
            const cancelRequest: CancelTaskRequest = {
                jsonrpc: "2.0",
                id: `req-${v4()}`,
                method: "tasks/cancel",
                params: { id: currentTaskId },
            };

            const response = await api.webui.post(`/api/v1/tasks/${currentTaskId}:cancel`, cancelRequest, { fullResponse: true });

            if (response.status === 202) {
                if (cancelTimeoutRef.current) clearTimeout(cancelTimeoutRef.current);
                cancelTimeoutRef.current = setTimeout(() => {
                    addNotification("Cancellation timed out. Allowing new input.");
                    setIsCancelling(false);
                    setIsResponding(false);
                    closeCurrentEventSource();
                    setCurrentTaskId(null);
                    cancelTimeoutRef.current = null;
                    setMessages(prev => prev.filter(msg => !msg.isStatusBubble));
                }, 15000);
            } else {
                const errorData = await response.json().catch(() => ({ message: "Unknown cancellation error" }));
                throw new Error(errorData.message || `HTTP error ${response.status}`);
            }
        } catch (error) {
            setError({ title: "Task Cancellation Failed", error: getErrorMessage(error, "An unknown error occurred.") });
            setIsCancelling(false);
        }
    }, [isResponding, isCancelling, currentTaskId, addNotification, setError, closeCurrentEventSource]);

    const handleFeedbackSubmit = useCallback(
        async (taskId: string, feedbackType: "up" | "down", feedbackText: string) => {
            if (!sessionId) {
                console.error("Cannot submit feedback without a session ID.");
                return;
            }
            try {
                await api.webui.post("/api/v1/feedback", {
                    taskId,
                    sessionId,
                    feedbackType,
                    feedbackText,
                });
                setSubmittedFeedback(prev => ({
                    ...prev,
                    [taskId]: { type: feedbackType, text: feedbackText },
                }));
            } catch (error) {
                console.error("Failed to submit feedback:", error);
                throw error;
            }
        },
        [sessionId]
    );

    const handleSseOpen = useCallback(() => {
        /* console.log for SSE open */
    }, []);

    const handleSseError = useCallback(() => {
        if (isResponding && !isFinalizing.current && !isCancellingRef.current) {
            setError({ title: "Connection Failed", error: "Connection lost. Please try again." });
        }
        if (!isFinalizing.current) {
            setIsResponding(false);
            if (!isCancellingRef.current) {
                closeCurrentEventSource();
                setCurrentTaskId(null);
            }
            latestStatusText.current = null;
        }
        setMessages(prev => prev.filter(msg => !msg.isStatusBubble).map((m, i, arr) => (i === arr.length - 1 && !m.isUser ? { ...m, isComplete: true } : m)));
    }, [closeCurrentEventSource, isResponding, setError]);

    const cleanupUploadedFiles = useCallback(async (uploadedFiles: Array<{ filename: string; sessionId: string }>) => {
        if (uploadedFiles.length === 0) {
            return;
        }

        for (const { filename, sessionId: fileSessionId } of uploadedFiles) {
            try {
                // Use the session ID that was used during upload
                await api.webui.delete(`/api/v1/artifacts/${fileSessionId}/${encodeURIComponent(filename)}`);
            } catch (error) {
                console.error(`[cleanupUploadedFiles] Exception while cleaning up file ${filename}:`, error);
                // Continue cleanup even if one fails (intentionally silent)
            }
        }
    }, []);

    const handleSubmit = useCallback(
        async (event: FormEvent, files?: File[] | null, userInputText?: string | null, overrideSessionId?: string | null, displayHtml?: string | null, contextQuote?: string | null, contextQuoteSourceId?: string | null) => {
            event.preventDefault();
            const currentInput = userInputText?.trim() || "";
            const currentFiles = files || [];
            if ((!currentInput && currentFiles.length === 0) || isResponding || isCancelling || !selectedAgentName) {
                return;
            }
            closeCurrentEventSource();
            isFinalizing.current = false;
            setIsResponding(true);
            setCurrentTaskId(null);
            latestStatusText.current = null;
            sseEventSequenceRef.current = 0;

            const userMsg: MessageFE = {
                role: "user",
                parts: [{ kind: "text", text: currentInput }],
                isUser: true,
                uploadedFiles: currentFiles.length > 0 ? currentFiles : undefined,
                displayHtml: displayHtml || undefined,
                contextQuote: contextQuote || undefined,
                contextQuoteSourceId: contextQuoteSourceId || undefined,
                metadata: {
                    messageId: `msg-${v4()}`,
                    sessionId: overrideSessionId || sessionId,
                    lastProcessedEventSequence: 0,
                },
            };
            latestStatusText.current = "Thinking";
            setMessages(prev => [...prev, userMsg]);

            try {
                // 1. Process files using hybrid approach with fail-fast
                const uploadedFileParts: FilePart[] = [];
                const successfullyUploadedFiles: Array<{ filename: string; sessionId: string }> = []; // Track large files for cleanup

                // Track the effective session ID for this message (may be updated if large file upload)
                // Use overrideSessionId if provided (e.g., from artifact upload that created a session)
                let effectiveSessionId = overrideSessionId || sessionId;

                console.log(`[handleSubmit] Processing ${currentFiles.length} file(s)`);

                for (const file of currentFiles) {
                    // Check if this is an artifact reference (pasted artifact)
                    if (file.type === "application/x-artifact-reference") {
                        try {
                            // Read the artifact reference data
                            const text = await file.text();
                            const artifactRef = JSON.parse(text);

                            if (artifactRef.isArtifactReference && artifactRef.uri) {
                                // This is a pasted artifact - send it as a file part with URI
                                console.log(`[handleSubmit] Adding artifact reference: ${artifactRef.filename} (${artifactRef.uri})`);
                                uploadedFileParts.push({
                                    kind: "file",
                                    file: {
                                        uri: artifactRef.uri,
                                        name: artifactRef.filename,
                                        mimeType: artifactRef.mimeType || "application/octet-stream",
                                    },
                                });
                                continue; // Skip to next file
                            }
                        } catch (error) {
                            console.error(`[handleSubmit] Error processing artifact reference:`, error);
                            // Fall through to normal file handling
                        }
                    }

                    if (file.size < INLINE_FILE_SIZE_LIMIT_BYTES) {
                        // Small file: send inline as base64 (no cleanup needed)
                        const base64Content = await fileToBase64(file);
                        uploadedFileParts.push({
                            kind: "file",
                            file: {
                                bytes: base64Content,
                                name: file.name,
                                mimeType: file.type,
                            },
                        });
                    } else {
                        // Large file: upload and get URI, pass effectiveSessionId to ensure all files go to the same session
                        const result = await uploadArtifactFile(file, effectiveSessionId);

                        // Check for success FIRST - must have both uri and sessionId
                        if (result && "uri" in result && result.uri && result.sessionId) {
                            // Update effective session ID once if backend has created a new session
                            if (!effectiveSessionId) {
                                effectiveSessionId = result.sessionId;
                            }

                            successfullyUploadedFiles.push({
                                filename: file.name,
                                sessionId: result.sessionId,
                            });

                            uploadedFileParts.push({
                                kind: "file",
                                file: {
                                    uri: result.uri,
                                    name: file.name,
                                    mimeType: file.type,
                                },
                            });
                        } else {
                            // ANY failure case (error object, null, or missing fields) - Clean up and stop
                            console.error(`[handleSubmit] File upload failed for "${file.name}". Result:`, result);
                            await cleanupUploadedFiles(successfullyUploadedFiles);

                            const cleanupMessage = successfullyUploadedFiles.length > 0 ? " Previously uploaded files have been cleaned up." : "";

                            const errorDetail = result && "error" in result ? ` (${result.error})` : "";
                            setError({ title: "File Upload Failed", error: `Message not sent. File upload failed for "${file.name}"${errorDetail}.${cleanupMessage}.` });
                            setIsResponding(false);
                            setMessages(prev => prev.filter(msg => msg.metadata?.messageId !== userMsg.metadata?.messageId));
                            return;
                        }
                    }
                }

                // 2. Construct message parts
                const messageParts: Part[] = [];
                if (currentInput) {
                    messageParts.push({ kind: "text", text: currentInput });
                }

                messageParts.push(...uploadedFileParts);

                if (messageParts.length === 0) {
                    return;
                }

                // 3. Construct the A2A message
                console.log(`ChatProvider handleSubmit: Using effectiveSessionId for contextId: ${effectiveSessionId}`);

                // Check if background execution is enabled via gateway config
                const enableBackgroundExecution = backgroundTasksEnabled ?? false;
                console.log(`[ChatProvider] Building metadata for ${selectedAgentName}, enableBackground=${enableBackgroundExecution}`);

                // Build metadata object
                const messageMetadata: Record<string, any> = {
                    agent_name: selectedAgentName,
                };

                if (activeProject?.id) {
                    messageMetadata.project_id = activeProject.id;
                }

                if (enableBackgroundExecution) {
                    messageMetadata.backgroundExecutionEnabled = true;
                    messageMetadata.maxExecutionTimeMs = backgroundTasksDefaultTimeoutMs ?? 3600000; // Default 1 hour
                    console.log(`[ChatProvider] Enabling background execution for ${selectedAgentName}`);
                    console.log(`[ChatProvider] Metadata object:`, messageMetadata);
                }

                const a2aMessage: Message = {
                    role: "user",
                    parts: messageParts,
                    messageId: `msg-${v4()}`,
                    kind: "message",
                    contextId: effectiveSessionId,
                    metadata: messageMetadata,
                };

                console.log(`[ChatProvider] A2A message metadata:`, a2aMessage.metadata);

                // 4. Construct the SendStreamingMessageRequest
                const sendMessageRequest: SendStreamingMessageRequest = {
                    jsonrpc: "2.0",
                    id: `req-${v4()}`,
                    method: "message/stream",
                    params: {
                        message: a2aMessage,
                    },
                };

                // 5. Send the request
                console.log("ChatProvider handleSubmit: Sending POST to /message:stream");
                const result: SendStreamingMessageSuccessResponse = await api.webui.post("/api/v1/message:stream", sendMessageRequest);

                const task = result?.result as Task | undefined;
                const taskId = task?.id;
                const responseSessionId = (task as Task & { contextId?: string })?.contextId;

                console.log(`ChatProvider handleSubmit: Extracted responseSessionId: ${responseSessionId}, current sessionId: ${sessionId}`);
                console.log(`ChatProvider handleSubmit: Full result object:`, result);

                if (!taskId) {
                    console.error("ChatProvider handleSubmit: Backend did not return a valid taskId. Result:", result);
                    throw new Error("Backend did not return a valid taskId.");
                }

                // Update session ID if backend provided one (for new sessions)
                console.log(`ChatProvider handleSubmit: Checking session update condition - responseSessionId: ${responseSessionId}, sessionId: ${sessionId}, different: ${responseSessionId !== sessionId}`);
                const isNewSession = !sessionId || sessionId === "";
                const finalSessionId = responseSessionId || sessionId;

                if (responseSessionId && responseSessionId !== sessionId) {
                    console.log(`ChatProvider handleSubmit: Updating sessionId from ${sessionId} to ${responseSessionId}`);
                    setSessionId(responseSessionId);
                    // Update the user message metadata with the new session ID
                    setMessages(prev => prev.map(msg => (msg.metadata?.messageId === userMsg.metadata?.messageId ? { ...msg, metadata: { ...msg.metadata, sessionId: responseSessionId } } : msg)));

                    // If it was a new session, generate and persist its name
                    if (isNewSession) {
                        let newSessionName = "New Chat";

                        // When auto title generation is DISABLED, use the user's message as the session name
                        // When auto title generation is ENABLED, keep "New Chat" as placeholder - LLM will generate the title
                        if (!autoTitleGenerationEnabled) {
                            const textParts = userMsg.parts.filter(p => p.kind === "text") as TextPart[];
                            const combinedText = textParts
                                .map(p => p.text)
                                .join(" ")
                                .trim();

                            if (combinedText) {
                                // Convert internal mention format @[Name](id) to display format @Name
                                // Also strip backend mention format <user_id:...> from the text
                                // The regex handles both complete (<user_id:id>) and truncated (<user_id:id) cases
                                let displayText = internalToDisplayText(combinedText);
                                displayText = displayText.replace(/<user_id:[^>]*>?/g, "").trim();
                                newSessionName = displayText.length > 100 ? `${displayText.substring(0, 100)}...` : displayText;
                            } else if (currentFiles.length > 0) {
                                // No text, but files were sent - derive name from files
                                if (currentFiles.length === 1) {
                                    newSessionName = currentFiles[0].name;
                                } else {
                                    newSessionName = `${currentFiles[0].name} +${currentFiles.length - 1} more`;
                                }
                            }
                        }

                        if (newSessionName) {
                            setSessionName(newSessionName);
                            await updateSessionName(responseSessionId, newSessionName);
                        }
                    }

                    // Trigger session list refresh for new sessions
                    if (isNewSession && typeof window !== "undefined") {
                        window.dispatchEvent(new CustomEvent("new-chat-session"));
                    }
                }

                // Save initial task with user message
                // For background tasks, we save with "pending" status so the session list shows the spinner
                // The backend TaskLoggerService will update this with the full response when complete
                const enabledForBackground = backgroundTasksEnabled ?? false;
                if (finalSessionId) {
                    await saveTaskToBackend(
                        {
                            task_id: taskId,
                            user_message: currentInput,
                            message_bubbles: [serializeMessageBubble(userMsg)],
                            task_metadata: {
                                schema_version: CURRENT_SCHEMA_VERSION,
                                status: "pending",
                                agent_name: selectedAgentName,
                                is_background_task: enabledForBackground,
                            },
                        },
                        finalSessionId
                    ); // Pass session ID explicitly
                }

                console.log(`ChatProvider handleSubmit: Received taskId ${taskId}. Setting currentTaskId and taskIdInSidePanel.`);
                setCurrentTaskId(taskId);
                setTaskIdInSidePanel(taskId);

                // Pre-register the task in the task monitor so it's available for visualization immediately
                // This prevents race conditions where the side panel tries to visualize before SSE events arrive
                const textParts = userMsg.parts.filter(p => p.kind === "text") as TextPart[];
                const initialRequestText =
                    textParts
                        .map(p => p.text)
                        .join(" ")
                        .trim() || "Task started...";
                registerTaskEarly(taskId, initialRequestText);

                // Check if this should be a background task (enabled via gateway config)
                if (enabledForBackground) {
                    console.log(`[ChatProvider] Registering ${taskId} as background task`);
                    registerBackgroundTask(taskId, finalSessionId, selectedAgentName);

                    // Trigger session list refresh to show spinner immediately
                    if (typeof window !== "undefined") {
                        window.dispatchEvent(new CustomEvent("new-chat-session"));
                    }
                }

                // Update user message with taskId so it's included in final save
                setMessages(prev => prev.map(msg => (msg.metadata?.messageId === userMsg.metadata?.messageId ? { ...msg, taskId: taskId } : msg)));
            } catch (error) {
                setError({ title: "Message Failed", error: getErrorMessage(error, "An error occurred. Please try again.") });
                setIsResponding(false);
                setMessages(prev => prev.filter(msg => !msg.isStatusBubble));
                setCurrentTaskId(null);
                isFinalizing.current = false;
                latestStatusText.current = null;
            }
        },
        [
            sessionId,
            isResponding,
            isCancelling,
            selectedAgentName,
            closeCurrentEventSource,
            uploadArtifactFile,
            saveTaskToBackend,
            serializeMessageBubble,
            activeProject,
            cleanupUploadedFiles,
            setError,
            backgroundTasksDefaultTimeoutMs,
            backgroundTasksEnabled,
            registerBackgroundTask,
            autoTitleGenerationEnabled,
            updateSessionName,
        ]
    );

    const prevProjectIdRef = useRef<string | null | undefined>("");
    const isSessionSwitchRef = useRef(false);
    const isSessionMoveRef = useRef(false);

    useEffect(() => {
        const handleProjectDeleted = (deletedProjectId: string) => {
            if (activeProject?.id === deletedProjectId) {
                console.log(`Project ${deletedProjectId} was deleted, clearing session context`);
                handleNewSession(false);
            }
        };

        registerProjectDeletedCallback(handleProjectDeleted);
    }, [activeProject, handleNewSession]);

    useEffect(() => {
        const handleTitleUpdated = async (event: Event) => {
            const customEvent = event as CustomEvent;
            const { sessionId: updatedSessionId } = customEvent.detail;

            // If the updated session is the current session, fetch and update the name
            if (updatedSessionId === sessionId) {
                try {
                    const sessionData = await api.webui.get(`/api/v1/sessions/${updatedSessionId}`);
                    const updatedSession = sessionData?.data;
                    if (updatedSession?.name) {
                        setSessionName(updatedSession.name);
                    }
                } catch (error) {
                    console.error("[ChatProvider] Error fetching updated session name:", error);
                }
            }
        };

        window.addEventListener("session-title-updated", handleTitleUpdated);
        return () => {
            window.removeEventListener("session-title-updated", handleTitleUpdated);
        };
    }, [sessionId]);

    useEffect(() => {
        const handleSessionUpdated = async (event: Event) => {
            const customEvent = event as CustomEvent;
            const { sessionId: updatedSessionId, projectId } = customEvent.detail;

            // Only handle if projectId is present (indicating a move)
            if (projectId === undefined) return;

            // If the updated session is the current session, update the project context
            if (updatedSessionId === sessionId) {
                // Set flag to prevent handleNewSession from being triggered by this project change
                isSessionMoveRef.current = true;

                if (projectId) {
                    // Session moved to a project - activate that project
                    const project = projects.find((p: Project) => p.id === projectId);
                    if (project) {
                        setActiveProject(project);
                    }
                } else {
                    // Session moved out of project - deactivate project context
                    setActiveProject(null);
                }
            }
        };

        window.addEventListener("session-updated", handleSessionUpdated);
        return () => {
            window.removeEventListener("session-updated", handleSessionUpdated);
        };
    }, [sessionId, projects, setActiveProject]);

    useEffect(() => {
        // Listen for background task completion events
        // When a background task completes, reload the session if it's currently active
        // If the user is on a different session, the buffer stays until they switch back
        // (buffer cleanup happens after replay + save)
        const handleBackgroundTaskCompleted = async (event: Event) => {
            const customEvent = event as CustomEvent;
            const { taskId: completedTaskId } = customEvent.detail;

            // Find the completed task
            const completedTask = backgroundTasksRef.current.find(t => t.taskId === completedTaskId);
            if (completedTask) {
                console.log(`[ChatProvider] Background task ${completedTaskId} completed for session ${completedTask.sessionId}`);

                // Only replay if the user is currently viewing the session where the task completed
                if (currentSessionIdRef.current === completedTask.sessionId) {
                    console.log(`[ChatProvider] User is on same session - will replay buffered events after delay`);
                    // Wait a bit to ensure any pending operations complete
                    setTimeout(async () => {
                        // Try to replay buffered events first (new single-path approach)
                        // This ensures embeds and templates are properly resolved through the frontend's SSE processing
                        const replayedSuccessfully = await replayBufferedEvents(completedTaskId);

                        if (!replayedSuccessfully) {
                            // Fall back to loading from chat_tasks if no buffered events
                            console.log(`[ChatProvider] No buffered events, falling back to loadSessionTasks`);
                            await loadSessionTasks(completedTask.sessionId);
                        }
                    }, 1500); // Delay to ensure save completes
                } else {
                    // User is on a different session - buffer stays until they switch back to that session
                    // When they switch back, handleSwitchSession will detect buffered events and replay them
                    console.log(`[ChatProvider] User is on different session (${currentSessionIdRef.current}) - buffer preserved for later replay when they switch back to ${completedTask.sessionId}`);
                }
            }
        };

        window.addEventListener("background-task-completed", handleBackgroundTaskCompleted);
        return () => {
            window.removeEventListener("background-task-completed", handleBackgroundTaskCompleted);
        };
    }, [loadSessionTasks, replayBufferedEvents]);

    useEffect(() => {
        // When the active project changes, reset the chat view to a clean slate
        // UNLESS the change was triggered by switching to a session (which handles its own state)
        // OR by moving a session (which should not start a new session)
        // Only trigger when activating or switching projects, not when deactivating (going to null)
        const prevId = prevProjectIdRef.current;
        const currentId = activeProject?.id;
        const isActivatingOrSwitching = currentId !== undefined && prevId !== currentId;

        if (isActivatingOrSwitching && !isSessionSwitchRef.current && !isSessionMoveRef.current) {
            console.log("Active project changed explicitly, resetting chat view and preserving project context.");
            handleNewSession(true); // Preserve the project context when switching projects
        }
        prevProjectIdRef.current = currentId;
        // Reset the flags after processing
        isSessionSwitchRef.current = false;
        isSessionMoveRef.current = false;
    }, [activeProject, handleNewSession]);

    useEffect(() => {
        // Don't show welcome message if we're loading a session
        if (!selectedAgentName && agents.length > 0 && messages.length === 0 && !isLoadingSession) {
            // Priority order for agent selection:
            // 1. URL parameter agent (?agent=AgentName)
            // 2. Project's default agent (if in project context)
            // 3. OrchestratorAgent (fallback)
            // 4. First available agent
            let selectedAgent = agents[0];

            // Check URL parameter first
            const urlParams = new URLSearchParams(window.location.search);
            const urlAgentName = urlParams.get("agent");
            let urlAgent: AgentCardInfo | undefined;

            if (urlAgentName) {
                urlAgent = agents.find(agent => agent.name === urlAgentName);
                if (urlAgent) {
                    selectedAgent = urlAgent;
                    console.log(`Using URL parameter agent: ${selectedAgent.name}`);
                } else {
                    console.warn(`URL parameter agent "${urlAgentName}" not found in available agents, falling back to priority order`);
                }
            }

            // If no URL agent found, follow existing priority order
            if (!urlAgent) {
                if (activeProject?.defaultAgentId) {
                    const projectDefaultAgent = agents.find(agent => agent.name === activeProject.defaultAgentId);
                    if (projectDefaultAgent) {
                        selectedAgent = projectDefaultAgent;
                        console.log(`Using project default agent: ${selectedAgent.name}`);
                    } else {
                        console.warn(`Project default agent "${activeProject.defaultAgentId}" not found, falling back to OrchestratorAgent`);
                        selectedAgent = agents.find(agent => agent.name === "OrchestratorAgent") ?? agents[0];
                    }
                } else {
                    selectedAgent = agents.find(agent => agent.name === "OrchestratorAgent") ?? agents[0];
                }
            }

            setSelectedAgentName(selectedAgent.name);

            const displayedText = configWelcomeMessage || `Hi! I'm the ${selectedAgent?.displayName}. How can I help?`;
            setMessages([
                {
                    parts: [{ kind: "text", text: displayedText }],
                    isUser: false,
                    isComplete: true,
                    role: "agent",
                    metadata: {
                        sessionId: "",
                        lastProcessedEventSequence: 0,
                    },
                },
            ]);
        }
    }, [agents, configWelcomeMessage, messages.length, selectedAgentName, sessionId, isLoadingSession, activeProject]);

    // Store the latest handlers in refs so they can be accessed without triggering effect re-runs
    // Note: handleSseMessageRef is declared earlier (line ~103) for use in loadSessionTasks
    const handleSseOpenRef = useRef(handleSseOpen);
    const handleSseErrorRef = useRef(handleSseError);

    // Update refs whenever handlers change (but this won't trigger the effect)
    useEffect(() => {
        handleSseMessageRef.current = handleSseMessage;
        handleSseOpenRef.current = handleSseOpen;
        handleSseErrorRef.current = handleSseError;
    }, [handleSseMessage, handleSseOpen, handleSseError]);

    useEffect(() => {
        if (currentTaskId) {
            const bearerToken = getApiBearerToken();

            const bgTask = backgroundTasksRef.current.find(t => t.taskId === currentTaskId);
            const isReconnecting = bgTask !== undefined;

            const params = new URLSearchParams();
            if (bearerToken) {
                params.append("token", bearerToken);
            }

            if (isReconnecting) {
                params.append("reconnect", "true");
                params.append("last_event_timestamp", "0");
                console.log(`[ChatProvider] Reconnecting to background task ${currentTaskId} - requesting full event replay`);

                setMessages(prev => {
                    const filtered = prev.filter(msg => {
                        if (msg.isUser) return true;
                        if (msg.taskId !== currentTaskId) return true;
                        return false;
                    });
                    return filtered;
                });
            }

            const baseUrl = api.webui.getFullUrl(`/api/v1/sse/subscribe/${currentTaskId}`);
            const eventSourceUrl = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
            const eventSource = new EventSource(eventSourceUrl, { withCredentials: true });
            currentEventSource.current = eventSource;

            const wrappedHandleSseOpen = () => {
                handleSseOpenRef.current();
            };

            const wrappedHandleSseError = () => {
                handleSseErrorRef.current();
            };

            const wrappedHandleSseMessage = (event: MessageEvent) => {
                if (handleSseMessageRef.current) {
                    handleSseMessageRef.current(event);
                }
            };

            eventSource.onopen = wrappedHandleSseOpen;
            eventSource.onerror = wrappedHandleSseError;
            eventSource.addEventListener("status_update", wrappedHandleSseMessage);
            eventSource.addEventListener("artifact_update", wrappedHandleSseMessage);
            eventSource.addEventListener("final_response", wrappedHandleSseMessage);
            eventSource.addEventListener("error", wrappedHandleSseMessage);

            return () => {
                // Explicitly remove listeners before closing
                eventSource.removeEventListener("status_update", wrappedHandleSseMessage);
                eventSource.removeEventListener("artifact_update", wrappedHandleSseMessage);
                eventSource.removeEventListener("final_response", wrappedHandleSseMessage);
                eventSource.removeEventListener("error", wrappedHandleSseMessage);
                eventSource.close();
            };
        } else {
            closeCurrentEventSource();
        }
    }, [currentTaskId, closeCurrentEventSource]);

    const contextValue: ChatContextValue = {
        ragData,
        ragEnabled,
        expandedDocumentFilename,
        setExpandedDocumentFilename,
        configCollectFeedback,
        submittedFeedback,
        handleFeedbackSubmit,
        sessionId,
        setSessionId,
        sessionName,
        setSessionName,
        messages,
        setMessages,
        isResponding,
        currentTaskId,
        isCancelling,
        latestStatusText,
        isLoadingSession,
        agents,
        agentsLoading,
        agentsError,
        agentsRefetch,
        agentNameDisplayNameMap,
        handleNewSession,
        handleSwitchSession,
        handleSubmit,
        handleCancel,
        notifications,
        addNotification,
        selectedAgentName,
        setSelectedAgentName,
        artifacts,
        artifactsLoading,
        artifactsRefetch,
        setArtifacts,
        uploadArtifactFile,
        isSidePanelCollapsed,
        activeSidePanelTab,
        setIsSidePanelCollapsed,
        setActiveSidePanelTab,
        openSidePanelTab,
        taskIdInSidePanel,
        setTaskIdInSidePanel,
        isDeleteModalOpen,
        artifactToDelete,
        openDeleteModal,
        closeDeleteModal,
        confirmDelete,
        openSessionDeleteModal,
        closeSessionDeleteModal,
        confirmSessionDelete,
        sessionToDelete,
        isArtifactEditMode,
        setIsArtifactEditMode,
        selectedArtifactFilenames,
        setSelectedArtifactFilenames,
        handleDeleteSelectedArtifacts,
        confirmBatchDeleteArtifacts,
        isBatchDeleteModalOpen,
        setIsBatchDeleteModalOpen,
        previewedArtifactAvailableVersions,
        currentPreviewedVersionNumber,
        previewFileContent,
        openArtifactForPreview: openPreview,
        navigateArtifactVersion: navigateToVersion,
        previewArtifact,
        setPreviewArtifact: setPreviewByArtifact,
        updateSessionName,
        deleteSession,

        /** Artifact Display and Cache Management */
        markArtifactAsDisplayed,
        downloadAndResolveArtifact,

        /** Global error display */
        displayError: setError,

        /** Pending prompt for starting new chat */
        pendingPrompt,
        startNewChatWithPrompt,
        clearPendingPrompt,

        /** Background Task Monitoring */
        backgroundTasks,
        backgroundNotifications,
        isTaskRunningInBackground,
    };

    // Handlers for the running task warning dialog
    const handleConfirmNavigation = useCallback(() => {
        setRunningTaskWarningOpen(false);
        if (pendingNavigationAction) {
            pendingNavigationAction();
            setPendingNavigationAction(null);
        }
    }, [pendingNavigationAction]);

    const handleCancelNavigation = useCallback(() => {
        setRunningTaskWarningOpen(false);
        setPendingNavigationAction(null);
    }, []);

    return (
        <ChatContext.Provider value={contextValue}>
            {children}
            <ErrorDialog />
            {/* Warning dialog when user tries to navigate away while a task is running and background tasks are disabled */}
            <ConfirmationDialog
                open={runningTaskWarningOpen}
                title="Task in Progress"
                description="A task is currently running. If you navigate away now, you may lose the response. Are you sure you want to leave?"
                onOpenChange={setRunningTaskWarningOpen}
                onConfirm={handleConfirmNavigation}
                onCancel={handleCancelNavigation}
                actionLabels={{
                    cancel: "Stay",
                    confirm: "Leave Anyway",
                }}
            />
        </ChatContext.Provider>
    );
};
