QuickNav for Google AI Studio

Restores classic chat navigation in Google AI Studio, adding essential UI controls for precise, message-by-message browsing, and a powerful message index menu for efficient conversation navigation. Now includes dynamically updating line numbers for code blocks! This script operates entirely locally in your browser, does not collect any personal data, and makes no requests to external servers.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         QuickNav for Google AI Studio
// @namespace    http://tampermonkey.net/
// @version      19.5
// @description  Restores classic chat navigation in Google AI Studio, adding essential UI controls for precise, message-by-message browsing, and a powerful message index menu for efficient conversation navigation. Now includes dynamically updating line numbers for code blocks! This script operates entirely locally in your browser, does not collect any personal data, and makes no requests to external servers.
// @author       Axl_script
// @homepageURL  https://greasyfork.org/en/scripts/548346-quicknav-for-google-ai-studio
// @contributionURL https://nowpayments.io/donation/axl_script
// @match        https://aistudio.google.com/*
// @grant        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

// --- MODULE: Chat Controller (The "Brain") ---
// Manages chat state, scrolling, observers, and turn indexing.
    const ChatController = {
        allTurns: [],
        currentIndex: -1,
        menuFocusedIndex: -1,
        isDownButtonAtEndToggle: false,
        JUMP_DISTANCE: 5,
        isScrollingProgrammatically: false,
        isQueueProcessing: false,
        isUnstickingFromBottom: false,
        originalScrollTop: 0,
        originalCurrentIndex: -1,
        loadingQueue: [],
        totalToLoad: 0,
        chatObserver: null,
        visibilityObserver: null,
        scrollObserver: null,
        chatContainerElement: null,
        holdTimeout: null,
        holdInterval: null,
        isBottomHoldActive: false,
        bottomHoldTimeout: null,
        bottomHoldInterval: null,
        debouncedBuildTurnIndex: null,
        visibleTurnContainers: new Set(),
        isScrollUpdateQueued: false,
        boundHandlePageScroll: null,
        isScrollLocked: false,
        scrollLockInterval: null,
        scrollLockTimeout: null,

        init(chatContainer) {
            this.chatContainerElement = chatContainer;
            this.debouncedBuildTurnIndex = this.debounce(this.buildTurnIndex.bind(this), 750);
            this.initializeChatObserver(this.chatContainerElement);
            this.buildTurnIndex();
            document.addEventListener('mousedown', this.stopBottomHold.bind(this), true);
            document.addEventListener('wheel', this.stopBottomHold.bind(this), true);
            document.addEventListener('click', this.handleEditClick.bind(this), true);

            const scrollContainer = this.chatContainerElement.querySelector('ms-autoscroll-container');
            if (scrollContainer) {
                this.boundHandlePageScroll = UIManager.handlePageScroll.bind(UIManager);
                scrollContainer.addEventListener('scroll', this.boundHandlePageScroll);

                this.boundHandleRealtimeScrollSync = this.handleRealtimeScrollSync.bind(this);
                scrollContainer.addEventListener('scroll', this.boundHandleRealtimeScrollSync);
            }
        },

        destroy() {
            if (this.chatObserver) this.chatObserver.disconnect();
            if (this.visibilityObserver) this.visibilityObserver.disconnect();
            if (this.scrollObserver) this.scrollObserver.disconnect();
            this.chatObserver = null;
            this.visibilityObserver = null;
            this.scrollObserver = null;
            this.clearScrollLock();

            if (this.chatContainerElement) {
                const scrollContainer = this.chatContainerElement.querySelector('ms-autoscroll-container');
                if (scrollContainer && this.boundHandlePageScroll) {
                    scrollContainer.removeEventListener('scroll', this.boundHandlePageScroll);
                }
            }

            this.chatContainerElement = null;
            this.allTurns = [];
            this.currentIndex = -1;
            this.menuFocusedIndex = -1;
            document.removeEventListener('mousedown', this.stopBottomHold.bind(this), true);
            document.removeEventListener('wheel', this.stopBottomHold.bind(this), true);
            document.removeEventListener('click', this.handleEditClick.bind(this), true);
        },

        clearScrollLock() {
            clearInterval(this.scrollLockInterval);
            clearTimeout(this.scrollLockTimeout);
            this.scrollLockInterval = null;
            this.scrollLockTimeout = null;
            this.isScrollLocked = false;
        },

        handleEditClick(event) {
            const editButton = event.target.closest('ms-chat-turn .toggle-edit-button');
            if (!editButton || this.isScrollLocked) return;

            const turnElement = event.target.closest('ms-chat-turn');
            const scrollContainer = this.chatContainerElement.querySelector('ms-autoscroll-container');
            if (!turnElement || !scrollContainer) return;

            const buttonIcon = editButton.querySelector('.ms-button-icon-symbol');
            const isEnteringEditMode = buttonIcon && buttonIcon.textContent.trim() !== 'done_all';
            if (!isEnteringEditMode) return;

            const savedScrollTop = scrollContainer.scrollTop;
            this.isScrollLocked = true;

            const immediateClearLock = () => {
                this.clearScrollLock();
                scrollContainer.removeEventListener('wheel', immediateClearLock, { once: true });
                scrollContainer.removeEventListener('mousedown', immediateClearLock, { once: true });
            };

            scrollContainer.addEventListener('wheel', immediateClearLock, { once: true });
            scrollContainer.addEventListener('mousedown', immediateClearLock, { once: true });

            this.scrollLockInterval = setInterval(() => {
                if (scrollContainer.scrollTop !== savedScrollTop) {
                    scrollContainer.scrollTop = savedScrollTop;
                }
            }, 15);

            this.scrollLockTimeout = setTimeout(() => {
                immediateClearLock();
            }, 5000);
        },

        debounce(func, wait) {
            let timeout;
            return function(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        },

        initializeChatObserver(container) {
            const observerCallback = (mutationsList) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList') {
                        let isRelevantMutation = false;
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType === 1) {
                                const codeBlock = node.matches('ms-code-block') ? node : node.querySelector('ms-code-block');
                                const chatTurn = node.matches('ms-chat-turn') ? node : node.querySelector('ms-chat-turn');
                                if (codeBlock) {
                                    const parentTurn = codeBlock.closest('ms-chat-turn');
                                    if (parentTurn) CodeBlockNavigator.processTurns([parentTurn]);
                                }
                                if (chatTurn || node.closest('ms-chat-turn')) {
                                    isRelevantMutation = true;
                                }
                            }
                        }
                        if (!isRelevantMutation) {
                            isRelevantMutation = Array.from(mutation.removedNodes).some(n => n.nodeType === 1 && n.matches('ms-chat-turn'));
                        }

                        if (isRelevantMutation) {
                            this.debouncedBuildTurnIndex();
                            return;
                        }
                    }
                }
            };
            this.chatObserver = new MutationObserver(observerCallback);
            this.chatObserver.observe(container, { childList: true, subtree: true });
        },

        setupVisibilityObserver() {
            if (this.visibilityObserver) {
                this.visibilityObserver.disconnect();
            }
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer || this.allTurns.length === 0) {
                return;
            }

            const observerCallback = (entries) => {
                for (const entry of entries) {
                    if (entry.isIntersecting) {
                        const visibleTurnElement = entry.target;
                        const turnIndex = this.allTurns.findIndex(t => t === visibleTurnElement);
                        if (turnIndex === -1) continue;

                        CodeBlockNavigator.processTurns([visibleTurnElement]);

                        const turnObject = this.allTurns[turnIndex];
                        const newContent = UIManager.getTextFromTurn(visibleTurnElement, true);

                        if (newContent.source === 'fallback') {
                            continue;
                        }

                        const hasValidCache = turnObject.cachedContent && !turnObject.isFallbackContent;
                        const isAnUpgrade = hasValidCache && turnObject.cachedContent.source === 'scrollbar' && newContent.source === 'dom';
                        const contentHasChanged = hasValidCache && turnObject.cachedContent.full !== newContent.full;

                        if (!hasValidCache || isAnUpgrade || contentHasChanged) {
                            turnObject.cachedContent = newContent;
                            turnObject.isFallbackContent = false;
                            UIManager.updateMenuItemContent(turnIndex);
                        }
                    }
                }
            };

            this.visibilityObserver = new IntersectionObserver(observerCallback, {
                root: scrollContainer,
                rootMargin: "0px",
            });

            this.allTurns.forEach(turn => this.visibilityObserver.observe(turn));
        },

        setupScrollObserver() {
            if (this.scrollObserver) this.scrollObserver.disconnect();
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer || this.allTurns.length === 0) return;

            const observerCallback = (entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) {
                        this.visibleTurnContainers.add(entry.target);
                    } else {
                        this.visibleTurnContainers.delete(entry.target);
                    }
                });
            };

            this.scrollObserver = new IntersectionObserver(observerCallback, {
                root: scrollContainer,
                rootMargin: "0px",
                threshold: 0
            });

            this.allTurns.forEach(turn => {
                const container = turn.querySelector('.chat-turn-container');
                if (container) {
                    this.scrollObserver.observe(container);
                }
            });
        },

        handleRealtimeScrollSync() {
            if (this.isScrollUpdateQueued || this.visibleTurnContainers.size === 0) {
                return;
            }
            this.isScrollUpdateQueued = true;

            requestAnimationFrame(() => {
                if (this.isScrollingProgrammatically || this.isQueueProcessing || this.isUnstickingFromBottom || this.isScrollLocked) {
                    this.isScrollUpdateQueued = false;
                    return;
                }

                const scrollContainer = this.chatContainerElement.querySelector('ms-autoscroll-container');
                if (!scrollContainer) {
                    this.isScrollUpdateQueued = false;
                    return;
                }

                const activationLine = scrollContainer.clientHeight * 0.3;
                let bestMatch = { element: null, minDistance: Infinity };

                this.visibleTurnContainers.forEach(containerElement => {
                    const rect = containerElement.getBoundingClientRect();
                    const distance = Math.abs(rect.top - activationLine);

                    if (distance < bestMatch.minDistance) {
                        bestMatch = { element: containerElement, minDistance: distance };
                    }
                });

                if (bestMatch.element) {
                    const parentTurn = bestMatch.element.closest('ms-chat-turn');
                    if (parentTurn) {
                        const bestMatchIndex = this.allTurns.indexOf(parentTurn);
                        if (bestMatchIndex !== -1 && this.currentIndex !== bestMatchIndex) {
                            UIManager.updateHighlight(this.currentIndex, bestMatchIndex);
                            this.currentIndex = bestMatchIndex;

                            const currentTurn = this.allTurns[bestMatchIndex];
                            if (currentTurn && (!currentTurn.cachedContent || currentTurn.isFallbackContent)) {
                                const newContent = UIManager.getTextFromTurn(currentTurn, true);
                                if (newContent.source !== 'fallback') {
                                    currentTurn.cachedContent = newContent;
                                    currentTurn.isFallbackContent = false;
                                }
                            }
                            UIManager.updateMenuItemContent(bestMatchIndex);

                            UIManager.updateCounterDisplay();
                            UIManager.showBadge();
                            UIManager.updateScrollPercentage();
                            UIManager.hideBadge();
                        }
                    }
                }
                this.isScrollUpdateQueued = false;
            });
        },

        isUserPrompt(turnElement) {
            return !!turnElement.querySelector('.chat-turn-container.user');
        },

        buildTurnIndex() {
            const isThinkingActive = !!document.querySelector('ms-thought-chunk');

            const freshTurns = Array.from(document.querySelectorAll('ms-chat-turn')).filter(turn => {
                const isUser = !!turn.querySelector('.chat-turn-container.user');
                if (isUser) {
                    return true;
                }

                const isModel = !!turn.querySelector('.chat-turn-container.model');
                if (isModel) {
                    const isThought = !!turn.querySelector('ms-thought-chunk');
                    const hasEditButton = !!turn.querySelector('.toggle-edit-button');
                    const contentContainer = turn.querySelector('.turn-content');
                    const hasRenderedContent = !!(contentContainer && contentContainer.querySelector('ms-prompt-chunk, ms-code-block'));

                    return !isThought && (hasEditButton || hasRenderedContent);
                }
                return false;
            });

            if (isThinkingActive && freshTurns.length < this.allTurns.length) {
                return;
            }


            if (freshTurns.length !== this.allTurns.length || !this.arraysEqual(this.allTurns, freshTurns)) {
                const wasOnLastTurn = this.currentIndex === this.allTurns.length - 1;
                const oldIndex = this.currentIndex;

                const freshTurnsSet = new Set(freshTurns);
                this.allTurns.forEach(oldTurn => {
                    if (!freshTurnsSet.has(oldTurn)) {
                        const container = oldTurn.querySelector('.chat-turn-container');
                        if (container) {
                            container.classList.remove('prompt-turn-highlight', 'response-turn-highlight');
                        }
                    }
                });

                const contentCache = new Map();
                this.allTurns.forEach(turn => {
                    if (turn.id && turn.cachedContent) {
                        contentCache.set(turn.id, { content: turn.cachedContent, isFallback: turn.isFallbackContent });
                    }
                });

                this.allTurns = freshTurns;

                this.allTurns.forEach(turn => {
                    if (turn.id && contentCache.has(turn.id)) {
                        const cachedData = contentCache.get(turn.id);
                        turn.cachedContent = cachedData.content;
                        turn.isFallbackContent = cachedData.isFallback;
                    } else {
                        turn.cachedContent = null;
                        turn.isFallbackContent = false;
                    }
                });

                if (wasOnLastTurn && this.allTurns.length > 0) {
                    const newLastIndex = this.allTurns.length - 1;
                    this.currentIndex = newLastIndex;
                    UIManager.updateHighlight(oldIndex, this.currentIndex);
                } else {
                     if (oldIndex >= 0 && oldIndex < this.allTurns.length) {
                         UIManager.updateHighlight(-1, oldIndex);
                     }
                }

                this.setupScrollObserver();
                this.setupVisibilityObserver();
            }

            CodeBlockNavigator.processTurns(this.allTurns);
            UIManager.updateCounterDisplay();
        },

        arraysEqual(a, b) {
            if (a.length !== b.length) return false;
            for (let i = 0; i < a.length; i++) {
                if (a[i] !== b[i]) return false;
            }
            return true;
        },

        setupHoldableButton(button, action) {
            const HOLD_DELAY = 300;
            const HOLD_INTERVAL = 100;
            let isHolding = false;
            const stopHold = () => {
                clearTimeout(this.holdTimeout);
                clearInterval(this.holdInterval);
                this.holdInterval = null;
                isHolding = false;
            };
            button.addEventListener('click', (e) => {
                if (!isHolding) {
                    action();
                }
                stopHold();
            });
            button.addEventListener('mousedown', (e) => {
                if (e.button !== 0) return;
                isHolding = true;
                this.holdTimeout = setTimeout(() => {
                    if (isHolding) {
                        this.holdInterval = setInterval(action, HOLD_INTERVAL);
                    }
                }, HOLD_DELAY);
            });
            button.addEventListener('mouseup', stopHold);
            button.addEventListener('mouseleave', stopHold);
        },

        scrollToAbsoluteTop() {
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer) return;
            UIManager.hideBadge(0, true);
            this.isScrollingProgrammatically = true;
            scrollContainer.scrollTo({
                top: 0,
                behavior: this.holdInterval ? 'auto' : 'smooth'
            });
            if (this.allTurns.length > 0 && this.currentIndex !== 0) {
                UIManager.updateHighlight(this.currentIndex, 0);
                this.currentIndex = 0;
                UIManager.updateCounterDisplay();
            }
            setTimeout(() => {
                this.isScrollingProgrammatically = false;
                UIManager.showBadge();
                UIManager.updateScrollPercentage();
                UIManager.hideBadge(2000);
                this.focusChatContainer();
            }, this.holdInterval ? 50 : 800);
        },

        waitForTurnToStabilize(turnElement, timeout = 1000) {
            return new Promise((resolve, reject) => {
                let lastRect = turnElement.getBoundingClientRect();
                let stableChecks = 0;
                const STABLE_CHECKS_REQUIRED = 3;
                const CHECK_INTERVAL = 100;
                const intervalId = setInterval(() => {
                    if (!turnElement || !turnElement.isConnected) {
                        clearInterval(intervalId);
                        clearTimeout(timeoutId);
                        return reject(new Error('Target element was removed from DOM.'));
                    }
                    const currentRect = turnElement.getBoundingClientRect();
                    if (currentRect.top !== lastRect.top || currentRect.height !== lastRect.height) {
                        lastRect = currentRect;
                        stableChecks = 0;
                    } else {
                        stableChecks++;
                    }
                    if (stableChecks >= STABLE_CHECKS_REQUIRED) {
                        clearInterval(intervalId);
                        clearTimeout(timeoutId);
                        resolve();
                    }
                }, CHECK_INTERVAL);
                const timeoutId = setTimeout(() => {
                    clearInterval(intervalId);
                    reject(new Error(`Element stabilization timed out.`));
                }, timeout);
            });
        },

        async scrollToTurn(index, blockPosition = 'center') {
            const targetTurn = this.allTurns[index];
            if (!targetTurn) {
                this.isScrollingProgrammatically = false;
                return;
            }
            this.isScrollingProgrammatically = true;
            const isSmooth = !this.isQueueProcessing && !this.holdInterval;
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer) {
                this.isScrollingProgrammatically = false;
                return;
            }
            try {
                const verticalOffset = scrollContainer.clientHeight * 0.35;

                let initialTargetScrollTop;
                if (blockPosition === 'end') {
                    initialTargetScrollTop = targetTurn.offsetTop - (scrollContainer.clientHeight - targetTurn.offsetHeight);
                } else {
                    initialTargetScrollTop = targetTurn.offsetTop - verticalOffset;
                }

                scrollContainer.scrollTop = Math.max(0, Math.min(initialTargetScrollTop, scrollContainer.scrollHeight - scrollContainer.clientHeight));
                await this.waitForTurnToStabilize(targetTurn, 4000);

                let finalTargetScrollTop;
                if (blockPosition === 'end') {
                    finalTargetScrollTop = targetTurn.offsetTop - (scrollContainer.clientHeight - targetTurn.offsetHeight);
                } else {
                    finalTargetScrollTop = targetTurn.offsetTop - verticalOffset;
                }

                finalTargetScrollTop = Math.max(0, finalTargetScrollTop);
                finalTargetScrollTop = Math.min(finalTargetScrollTop, scrollContainer.scrollHeight - scrollContainer.clientHeight);
                scrollContainer.scrollTo({
                    top: finalTargetScrollTop,
                    behavior: isSmooth ? 'smooth' : 'auto'
                });
                const timeoutDuration = isSmooth ? 800 : 50;
                await new Promise(resolve => setTimeout(resolve, timeoutDuration));
            } catch (error) {} finally {
                this.isScrollingProgrammatically = false;
                UIManager.showBadge();
                UIManager.updateScrollPercentage();
                UIManager.hideBadge(2000);
            }
        },

        async navigateToIndex(newIndex, blockPosition = 'center') {
            if (newIndex < 0 || newIndex >= this.allTurns.length) return;
            UIManager.hideBadge(0, true);
            const oldIndex = this.currentIndex;
            if (newIndex < this.allTurns.length - 1) this.isDownButtonAtEndToggle = false;
            this.currentIndex = newIndex;

            UIManager.updateHighlight(oldIndex, newIndex);
            UIManager.updateCounterDisplay();
            await this.scrollToTurn(newIndex, blockPosition);

            const targetTurn = this.allTurns[newIndex];
            if (targetTurn) {
                const newContent = UIManager.getTextFromTurn(targetTurn, true);
                if (newContent.source !== 'fallback') {
                    targetTurn.cachedContent = newContent;
                    targetTurn.isFallbackContent = false;
                    UIManager.updateMenuItemContent(newIndex);
                }
            }

            UIManager.updateScrollPercentage();
            this.focusChatContainer();
        },

        async forceScrollToTop(scrollContainer) {
            return new Promise(resolve => {
                this.isScrollingProgrammatically = true;
                const firstTurn = this.allTurns[0];
                if (!firstTurn) {
                    this.isScrollingProgrammatically = false;
                    return resolve();
                }
                let attempts = 0;
                const maxAttempts = 15;
                const attemptScroll = () => {
                    attempts++;
                    scrollContainer.scrollTop = 0;
                    setTimeout(() => {
                        if (scrollContainer.scrollTop < 50 || attempts >= maxAttempts) {
                            firstTurn.scrollIntoView({ behavior: 'auto', block: 'start' });
                            setTimeout(() => { this.isScrollingProgrammatically = false; resolve(); }, 100);
                        } else {
                            attemptScroll();
                        }
                    }, 150);
                };
                attemptScroll();
            });
        },

        async startDynamicMenuLoading() {
            if (this.isQueueProcessing) return;
            const loadButton = document.getElementById('chat-nav-load-button');
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer || !loadButton) return;
            const menuList = document.getElementById('chat-nav-menu');
            const menuItems = menuList ? menuList.querySelectorAll('.chat-nav-menu-item') : [];
            if (menuList && menuItems.length > 0) {
                const targetIndex = this.currentIndex > -1 ? this.currentIndex : 0;
                UIManager.updateMenuFocus(menuItems, targetIndex, true);
                menuList.focus({ preventScroll: true });
            }
            this.originalCurrentIndex = this.currentIndex;
            this.originalScrollTop = scrollContainer.scrollTop;
            this.loadingQueue = this.allTurns
                .map((turn, index) => ({ turn, index, menuItem: menuItems[index] }))
                .filter(item => {
                    const text = item.menuItem.dataset.tooltip;
                    return text.endsWith('...') || text === 'Could not extract content.' || !item.turn.cachedContent || item.turn.isFallbackContent;
                });
            if (this.loadingQueue.length > 0) {
                this.totalToLoad = this.loadingQueue.length;
                loadButton.disabled = true;
                loadButton.classList.add('loading-active');
                this.isQueueProcessing = true;

                const isAtBottom = scrollContainer.scrollTop >= scrollContainer.scrollHeight - scrollContainer.clientHeight - 5;
                if (isAtBottom) {
                    this.isUnstickingFromBottom = true;
                    scrollContainer.scrollTop -= 10;
                    await new Promise(resolve => setTimeout(resolve, 50));
                    this.isUnstickingFromBottom = false;
                }

                await this.forceScrollToTop(scrollContainer);
                this.processLoadingQueue();
            } else {
                const statusIndicator = document.getElementById('chat-nav-loader-status');
                if (statusIndicator) {
                    statusIndicator.textContent = 'All loaded.';
                    statusIndicator.classList.add('google-text-flash');
                    statusIndicator.addEventListener('animationend', () => {
                        statusIndicator.classList.remove('google-text-flash');
                    }, { once: true });
                }
            }
        },

        pollForContent(turn) {
            return new Promise((resolve, reject) => {
                const maxAttempts = 50;
                let attempts = 0;
                const interval = setInterval(() => {
                    if (!this.isQueueProcessing) {
                        clearInterval(interval);
                        return reject(new Error('Loading stopped by user.'));
                    }
                    const content = UIManager.getTextFromTurn(turn, true);
                    if (content.source === 'dom') {
                        clearInterval(interval);
                        resolve(content);
                    } else if (++attempts >= maxAttempts) {
                        clearInterval(interval);
                        reject(new Error('Content polling timed out.'));
                    }
                }, 100);
            });
        },

        async processLoadingQueue() {
            const statusIndicator = document.getElementById('chat-nav-loader-status');
            const menuItems = document.querySelectorAll('#chat-nav-menu .chat-nav-menu-item');
            while (this.loadingQueue.length > 0 && this.isQueueProcessing) {
                const itemsProcessed = this.totalToLoad - this.loadingQueue.length;
                if (statusIndicator) {
                    statusIndicator.textContent = `Loading ${itemsProcessed + 1} of ${this.totalToLoad}...`;
                    statusIndicator.classList.remove('google-text-flash');
                }
                const itemToLoad = this.loadingQueue.shift();
                const { turn, index, menuItem } = itemToLoad;
                const textSpan = menuItem.querySelector('.menu-item-text');
                if (!turn || !textSpan) continue;
                menuItem.classList.add('loading-in-progress');
                UIManager.updateMenuFocus(menuItems, index, true);
                try {
                    await this.scrollToTurn(index, 'center');
                    const newContent = await this.pollForContent(turn);
                    turn.cachedContent = newContent;
                    turn.isFallbackContent = false;
                    const truncatedText = (newContent.display.length > 200) ? newContent.display.substring(0, 197) + '...' : newContent.display;
                    textSpan.textContent = truncatedText;
                    menuItem.dataset.tooltip = newContent.full.replace(/\s+/g, ' ');
                } catch (error) {
                    console.error(`Failed to load item ${index + 1}:`, error.message);
                    textSpan.textContent = '[Error]';
                } finally {
                    menuItem.classList.remove('loading-in-progress');
                }
            }
            this.isQueueProcessing = false;
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (this.originalCurrentIndex > -1 && this.originalCurrentIndex < this.allTurns.length) {
                await this.navigateToIndex(this.originalCurrentIndex, 'center');
            } else if (scrollContainer) {
                this.isScrollingProgrammatically = true;
                scrollContainer.scrollTo({ top: this.originalScrollTop, behavior: 'smooth' });
                await new Promise(resolve => setTimeout(() => {
                    this.isScrollingProgrammatically = false;
                    resolve();
                }, 800));
            }
            const menuContainer = document.getElementById('chat-nav-menu-container');
            const loadButton = document.getElementById('chat-nav-load-button');
            if (loadButton) {
                loadButton.disabled = false;
                loadButton.classList.remove('loading-active');
            }
            if (statusIndicator) {
                if (this.loadingQueue.length > 0) {
                    statusIndicator.textContent = 'Stopped.';
                    statusIndicator.classList.remove('google-text-flash');
                } else {
                    statusIndicator.textContent = 'Done.';
                    statusIndicator.classList.add('google-text-flash');
                    statusIndicator.addEventListener('animationend', () => {
                        statusIndicator.classList.remove('google-text-flash');
                    }, { once: true });
                }
            }

            if (menuContainer && menuContainer.classList.contains('visible')) {
                const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
                UIManager.updateMenuFocus(items, this.currentIndex, true);
                const menuList = document.getElementById('chat-nav-menu');
                if (menuList) {
                    menuList.focus({ preventScroll: true });
                }
            } else {
                this.focusChatContainer();
            }
        },

        stopDynamicMenuLoading() {
            if (!this.isQueueProcessing) return;
            this.isQueueProcessing = false;
            const loadButton = document.getElementById('chat-nav-load-button');
            if (loadButton) {
                loadButton.disabled = false;
                loadButton.classList.remove('loading-active');
            }
            const statusIndicator = document.getElementById('chat-nav-loader-status');
            if (statusIndicator) {
                statusIndicator.textContent = 'Stopped.';
                statusIndicator.classList.remove('google-text-animated');
            }
        },

        stopBottomHold() {
            if (!this.isBottomHoldActive) return;
            this.isBottomHoldActive = false;
            clearInterval(this.bottomHoldInterval);
            this.bottomHoldInterval = null;
            const btnBottom = document.getElementById('nav-bottom');
            if (btnBottom) {
                btnBottom.classList.remove('auto-click-active');
            }
        },

        focusChatContainer() {
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (scrollContainer) {
                scrollContainer.tabIndex = -1;
                scrollContainer.focus({ preventScroll: true });
            }
        },

        focusPromptInput() {
            const targetElement = document.querySelector('ms-prompt-input-wrapper textarea');
            if (!targetElement) {
                console.error("QuickNav: Prompt input textarea not found.");
                return;
            }
            const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window });
            const mouseUpEvent = new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window });
            const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true, view: window });
            targetElement.dispatchEvent(mouseDownEvent);
            targetElement.dispatchEvent(mouseUpEvent);
            targetElement.dispatchEvent(clickEvent);
            targetElement.focus();
        }
    };

// --- MODULE: UI Manager ---
    const UIManager = {
        customTooltip: null,
        tooltipTimeout: null,
        badgeFadeTimeout: null,
        lastScrollTime: 0,
        lastScrollTop: 0,
        isHoveringOnNav: false,
        isThrottled: false,
        footerElement: null,
        toolbarElement: null,
        navContainerElement: null,
        listenedTurnElement: null,
        _handleTurnMouseMove: null,
        _handleTurnMouseLeave: null,

        create(targetNode) {
            this.injectStyles();
            this.createGlobalElements();
            this.createAndInjectUI(targetNode);
            this.cacheStaticElements();
            this._handleTurnMouseMove = this._handleTurnMouseMove.bind(this);
            this._handleTurnMouseLeave = this._handleTurnMouseLeave.bind(this);
        },

        destroy() {
            const ui = document.getElementById('chat-nav-container');
            if (ui) ui.remove();
            const menu = document.getElementById('chat-nav-menu-container');
            if (menu) menu.remove();
            const badge = document.getElementById('quicknav-badge-floater');
            if (badge) badge.remove();
            const tooltip = document.getElementById('quicknav-custom-tooltip');
            if (tooltip) tooltip.remove();
            this.customTooltip = null;
            this.footerElement = null;
            this.toolbarElement = null;
            this.navContainerElement = null;
            if (this.listenedTurnElement) {
                this.listenedTurnElement.removeEventListener('mousemove', this._handleTurnMouseMove);
                this.listenedTurnElement.removeEventListener('mouseleave', this._handleTurnMouseLeave);
                this.listenedTurnElement = null;
            }
        },

        cacheStaticElements() {
            this.footerElement = document.querySelector('section.chunk-editor-main > footer');
            this.toolbarElement = document.querySelector('ms-toolbar');
            this.navContainerElement = document.getElementById('chat-nav-container');
        },

        showBadge() {
            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater) return;
            this.cancelHideBadge();
            floater.style.opacity = '1';
        },

// Hides the badge with an optional delay and a force flag to override hover state.
        hideBadge(delay = 1000, force = false) {
            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater || (this.isHoveringOnNav && !force)) return;

            this.cancelHideBadge();

            if (force) {
                floater.classList.add('quicknav-badge-notransition');
                floater.style.opacity = '0';
                requestAnimationFrame(() => {
                    floater.classList.remove('quicknav-badge-notransition');
                });
            } else {
                this.badgeFadeTimeout = setTimeout(() => {
                    floater.style.opacity = '0';
                }, delay);
            }
        },

        cancelHideBadge() {
            clearTimeout(this.badgeFadeTimeout);
        },

        // Sets up an observer to detect which message is currently in the viewport.
        setupMessageObserver() {
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            const messages = document.querySelectorAll('.message-turn');
            if (!scrollContainer || !messages.length) return;

            const observerOptions = {
                root: scrollContainer,
                rootMargin: '0px',
                threshold: 0.1
            };

            const observer = new IntersectionObserver(this.handleMessageVisibilityChange.bind(this), observerOptions);
            messages.forEach(msg => observer.observe(msg));
        },

        // Handles the visibility change event from the IntersectionObserver.
        handleMessageVisibilityChange(entries) {
            const visibleEntries = entries.filter(entry => entry.isIntersecting);

            if (visibleEntries.length === 0) return;

            const bottomMostEntry = visibleEntries.reduce((prev, current) => {
                return prev.boundingClientRect.top > current.boundingClientRect.top ? prev : current;
            });

            if (bottomMostEntry && bottomMostEntry.target !== this.currentVisibleMessage) {
                this.currentVisibleMessage = bottomMostEntry.target;
                this.showBadge();
                this.updateScrollPercentage();
                this.hideBadge();
            }
        },

// Shows the badge when scroll speed exceeds a certain threshold.
        handlePageScroll() {
            if (this.isThrottled) {
                return;
            }
            this.isThrottled = true;

            const THROTTLE_INTERVAL = 200;
            setTimeout(() => {
                this.isThrottled = false;
            }, THROTTLE_INTERVAL);

            const SCROLL_SPEED_THRESHOLD = 500;
            const scrollContainer = document.querySelector('ms-autoscroll-container');
            if (!scrollContainer) return;

            const currentTime = performance.now();
            const currentScrollTop = scrollContainer.scrollTop;

            const deltaTime = currentTime - this.lastScrollTime;

            if (deltaTime > 0) {
                const deltaScroll = currentScrollTop - this.lastScrollTop;
                const scrollSpeed = Math.abs(deltaScroll / deltaTime) * 1000;

                if (scrollSpeed > SCROLL_SPEED_THRESHOLD) {
                    this.showBadge();
                    this.updateScrollPercentage();
                    this.hideBadge(1000);
                }
            }

            this.lastScrollTop = currentScrollTop;
            this.lastScrollTime = currentTime;
        },

        createGlobalElements() {
            if (!document.getElementById('quicknav-badge-floater')) {
                const badgeFloater = document.createElement('div');
                badgeFloater.id = 'quicknav-badge-floater';
                const badgeIndex = document.createElement('div');
                badgeIndex.id = 'quicknav-badge-index';
                const badgePercentage = document.createElement('div');
                badgePercentage.id = 'quicknav-badge-percentage';
                badgeFloater.append(badgeIndex, badgePercentage);
                document.body.appendChild(badgeFloater);
            }
            if (!document.getElementById('quicknav-custom-tooltip')) {
                this.customTooltip = document.createElement('div');
                this.customTooltip.id = 'quicknav-custom-tooltip';
                document.body.appendChild(this.customTooltip);
            }
        },

        debounce(func, wait) {
            let timeout;
            return function(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        },

// Injects the CSS for the UI elements into the document head.
        injectStyles() {
            if (document.getElementById('chat-nav-styles')) return;
            const styleSheet = document.createElement("style");
            styleSheet.id = 'chat-nav-styles';
            styleSheet.textContent = `
                .chat-turn-container {}

                @keyframes google-text-flow { to { background-position: 200% center; } }
                @keyframes donate-blue-flow { to { background-position: 200% center; } }
                @keyframes load-all-flow { to { background-position: 200% center; } }
                @keyframes quicknav-blinking { 0%, 100% { background-color: #d1c4e9; box-shadow: 0 0 4px #b39ddb; } 50% { background-color: #b39ddb; box-shadow: 0 0 6px 1px #d1c4e9; } }
                @keyframes quicknav-loading-flow { to { background-position: -200% center; } }
                .google-text-animated, .google-text-flash { background: linear-gradient(90deg, #8ab4f8, #e67c73, #f7cb73, #57bb8a, #8ab4f8); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; color: transparent !important; }
                .google-text-animated { animation: google-text-flow 10s linear infinite; }
                .google-text-flash { animation: google-text-flow 1.5s ease-in-out 1; }
                .donate-button-animated { background: linear-gradient(90deg, #a8c7fa, #8ab4f8, #669df6, #8ab4f8, #a8c7fa); background-size: 200% auto; -webkit-background-clip: text; background-clip: text; color: transparent !important; animation: donate-blue-flow 8s ease-in-out infinite; }
                ms-autoscroll-container:focus { outline: none; }
                #chat-nav-container { display: flex; justify-content: center; align-items: center; gap: 12px; margin: 2px auto; width: 100%; box-sizing: border-box; position: relative; z-index: 2147483647; }
                .counter-wrapper { position: relative; pointer-events: none; z-index: 9999; }
                .chat-nav-button, #chat-nav-counter { background-color: transparent; border: 1px solid var(--ms-on-surface-variant, #888888); transition: transform 0.1s ease-out, background-color 0.15s ease-in-out; pointer-events: auto; user-select: none; cursor: pointer; }
                .chat-nav-button { color: var(--ms-on-surface-variant, #888888); flex-shrink: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 50%; }
                #nav-up, #nav-down { color: #8ab4f8; border-color: #669df6; }
                #nav-top, #nav-bottom, #chat-nav-counter { border-color: #5f6368; }
                #chat-nav-counter { font-family: 'Google Sans', sans-serif; font-size: 12px; padding: 4px 8px; border-radius: 8px; display: inline-flex; align-items: baseline; color: var(--ms-on-surface-variant, #888888); }
                .chat-nav-button:hover, .chat-nav-button:focus { background-color: var(--ms-surface-3, #F1F3F4); outline: none; }
                #nav-top:hover, #nav-bottom:hover, #nav-top:focus, #nav-bottom:focus { background-color: rgba(136, 136, 136, 0.15); }
                #nav-up:hover, #nav-down:hover, #nav-up:focus, #nav-down:focus, #chat-nav-counter:hover, #chat-nav-counter:focus { background-color: rgba(138, 180, 248, 0.15); outline: none; }
                .chat-nav-button:active { transform: scale(0.95); }
                #nav-bottom.auto-click-active { border-color: #9575cd; color: #4527a0; animation: quicknav-blinking 1.2s ease-in-out infinite; }
                #chat-nav-current-num.chat-nav-current-grey { color: var(--ms-on-surface-variant, #888888); }
                #chat-nav-current-num.chat-nav-current-blue { color: #8ab4f8; font-weight: 500; }
                #chat-nav-total-num { color: #8ab4f8; font-weight: 500; }
                .prompt-turn-highlight { box-shadow: inset 0 0 0 1px var(--ms-on-surface-variant, #9aa0a6) !important; }
                .response-turn-highlight { box-shadow: inset 0 0 0 1px var(--ms-primary, #8ab4f8) !important; }
                .quicknav-title { flex: 2; text-align: center; font-family: 'Google Sans', 'Inter Tight', sans-serif; font-size: 14px; user-select: text; font-weight: 600; }

                /* --- MENU & TOOLTIP --- */
                #chat-nav-menu-container { background-color: #f1f3f4; border: 2px solid #1a73e8; }
                #chat-nav-menu { background-color: #f1f3f4; }
                .chat-nav-menu-item { background-color: #ffffff; color: #202124; }
                .chat-nav-menu-item:hover { background-color: #e8eaed; }
                .chat-nav-menu-item.menu-item-focused { background-color: #dfe1e5; }
                .chat-nav-menu-item.loading-in-progress, .chat-nav-menu-item.loading-in-progress:hover, .chat-nav-menu-item.loading-in-progress.menu-item-focused { background: linear-gradient(100deg, #f1f3f4 20%, #d2e3fc 40%, #d2e3fc 60%, #f1f3f4 80%); background-size: 200% 100%; animation: quicknav-loading-flow 1.8s linear infinite; }
                .chat-nav-menu-header { background-color: #f1f3f4; border-bottom: 1px solid #1a73e8; }
                .header-button { font-family: 'Google Sans', sans-serif; text-decoration: none; font-size: 12px; padding: 4px 10px; border-radius: 16px; transition: background-color 0.15s ease-in-out, opacity 0.15s ease-in-out, box-shadow 0.15s ease-in-out; border: 1px solid #669df6; cursor: pointer; font-weight: 500; }
                .header-button:disabled { opacity: 0.5; cursor: not-allowed; }
                .header-button:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--ms-surface-1, #ffffff), 0 0 0 4px var(--ms-primary, #8ab4f8); }
                #chat-nav-load-button { background-color: #e8f0fe; color: #1967d2; }
                #chat-nav-load-button:hover:not(:disabled) { background-color: #d2e3fc; }
                #chat-nav-load-button.loading-active { color: #ffffff; background: linear-gradient(90deg, #1967d2, #4285f4, #1967d2); background-size: 200% auto; animation: load-all-flow 2s linear infinite; }
                #chat-nav-menu::-webkit-scrollbar-track { background: #e8eaed; }
                #chat-nav-menu::-webkit-scrollbar-thumb { background-color: #dfe1e5; }
                #chat-nav-menu::-webkit-scrollbar-thumb:hover { background-color: #9aa0a6; }
                #quicknav-custom-tooltip { background-color: #f1f3f4; color: #202124; border: 1px solid #dadce0; }
                .response-item-bg .menu-item-text { color: #174ea6; }
                body.dark-theme #chat-nav-menu-container { background-color: #191919; border-color: #8ab4f8; }
                body.dark-theme #chat-nav-menu { background-color: #191919; }
                body.dark-theme .chat-nav-menu-item { background-color: #202124; color: #e8eaed; }
                body.dark-theme .chat-nav-menu-item:hover { background-color: #3c4043; }
                body.dark-theme .chat-nav-menu-item.menu-item-focused { background-color: #5f6368; }
                body.dark-theme .chat-nav-menu-item.loading-in-progress, body.dark-theme .chat-nav-menu-item.loading-in-progress:hover, body.dark-theme .chat-nav-menu-item.loading-in-progress.menu-item-focused { background: linear-gradient(100deg, #202124 20%, #3c4043 40%, #3c4043 60%, #202124 80%); background-size: 200% 100%; animation: quicknav-loading-flow 1.8s linear infinite; }
                body.dark-theme .chat-nav-menu-header { background-color: #191919; border-color: #8ab4f8; }
                body.dark-theme .header-button { background-color: #3c4043; color: #e8eaed; border-color: #8ab4f8; }
                body.dark-theme #chat-nav-load-button { background-color: #28354a; color: #a8c7fa; }
                body.dark-theme .header-button:hover:not(:disabled) { background-color: #5f6368; }
                body.dark-theme #chat-nav-load-button:hover:not(:disabled) { background-color: #3c4043; }
                body.dark-theme #chat-nav-load-button.loading-active { color: #202124; background: linear-gradient(90deg, #8ab4f8, #a8c7fa, #8ab4f8); background-size: 200% auto; animation: load-all-flow 2s linear infinite; }
                body.dark-theme #chat-nav-menu::-webkit-scrollbar-track { background: #202124; }
                body.dark-theme #chat-nav-menu::-webkit-scrollbar-thumb { background-color: #5f6368; }
                body.dark-theme #chat-nav-menu::-webkit-scrollbar-thumb:hover { background-color: #9aa0a6; }
                body.dark-theme #quicknav-custom-tooltip { background-color: #2d2d2d; color: #e0e0e0; border: 1px solid #555; }
                body.dark-theme .response-item-bg .menu-item-text { color: var(--ms-primary, #8ab4f8); }
                #quicknav-badge-floater { position: fixed; z-index: 99998; opacity: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; pointer-events: none; padding: 4px 3px; border-radius: 8px; font-family: 'Google Sans', sans-serif; transition: opacity 0.15s ease-in-out; min-width: 28px; box-sizing: border-box; }
                .quicknav-badge-notransition { transition: none !important; }
                #quicknav-badge-index { font-size: 13px; font-weight: 500; line-height: 1.2; }
                #quicknav-badge-percentage { font-size: 10px; font-weight: 400; line-height: 1.2; border-top: 1px solid rgba(255, 255, 255, 0.3); margin-top: 3px; padding-top: 3px; }
                .prompt-badge-bg { background-color: #5f6368; color: #FFFFFF; }
                .response-badge-bg { background-color: #174ea6; color: #FFFFFF; }
                #chat-nav-menu-container { display: flex; flex-direction: column; position: fixed; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); max-height: 90vh; z-index: 99999; max-width: 800px; min-width: 300px; box-sizing: border-box; visibility: hidden; opacity: 0; pointer-events: none; transition: opacity 0.15s ease-in-out, visibility 0s linear 0.15s; }
                #chat-nav-menu-container.visible { visibility: visible; opacity: 1; pointer-events: auto; transition: opacity 0.15s ease-in-out, visibility 0s linear 0s; }
                #chat-nav-menu-container:focus { outline: none; }
                #chat-nav-menu { list-style: none; margin: 0; padding: 0 8px 8px 8px; overflow-y: auto; scroll-behavior: smooth; flex-grow: 1; border-radius: 0 0 10px 10px; }
                .chat-nav-menu-item { display: flex; align-items: center; padding: 8px 12px; margin: 2px 0; border-radius: 8px; cursor: pointer; font-size: 13px; font-family: 'Google Sans', sans-serif; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
                .menu-item-number { font-weight: 500; margin-right: 8px; flex-shrink: 0; }
                .prompt-number-color { color: var(--ms-on-surface-variant, #9aa0a6); }
                .response-number-color { color: var(--ms-primary, #8ab4f8); }
                .menu-item-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
                .prompt-item-bg { border-left: 3px solid var(--ms-on-surface-variant, #9aa0a6); margin-left: 32px; }
                .response-item-bg { border-left: 3px solid var(--ms-primary, #8ab4f8); border-bottom: 1px solid var(--ms-primary, #8ab4f8); }
                .chat-nav-menu-header { flex-shrink: 0; z-index: 1; display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-radius: 10px 10px 0 0; }
                .header-controls { flex: 1; display: flex; align-items: center; gap: 8px; }
                .header-controls.left { justify-content: flex-start; }
                .header-controls.right { justify-content: flex-end; }
                #chat-nav-menu::-webkit-scrollbar { width: 8px; }
                #chat-nav-loader-status { font-family: 'Google Sans', sans-serif; font-size: 12px; color: var(--ms-on-surface-variant, #9aa0a6); padding: 4px 10px; font-weight: 600; }
                #quicknav-custom-tooltip { position: fixed; z-index: 100000; border-radius: 6px; padding: 6px 10px; font-size: 13px; font-family: 'Google Sans', sans-serif; max-width: 40vw; pointer-events: none; opacity: 0; transition: opacity 0.15s ease-in-out; white-space: normal; }

                /* --- CODE BLOCK NAVIGATION (Google Blue Theme) --- */
                .code-block-nav-container { display: flex; align-items: center; gap: 8px; margin-left: auto; }
                .code-nav-button { background: transparent; border: 1px solid #669df6; color: #8ab4f8; height: 26px; border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: background-color 0.15s ease, transform 0.1s ease, border-color 0.15s ease, color 0.15s ease; padding: 0 6px; }
                .code-nav-counter { font-family: 'Google Sans', sans-serif; font-size: 12px; font-weight: 500; color: #8ab4f8; user-select: none; }
                .code-nav-button:hover:not(:disabled) { background-color: rgba(138, 180, 248, 0.15); }
                .code-nav-button:active:not(:disabled) { transform: scale(0.95); }
                .code-nav-button:disabled { opacity: 0.5; cursor: not-allowed; color: var(--ms-on-surface-variant, #9aa0a6); border-color: var(--ms-on-surface-variant, #9aa0a6); }
            `;
            document.head.appendChild(styleSheet);
        },

// Creates the main navigation UI and injects it into the page.
        createAndInjectUI(targetNode) {
            const navContainer = document.createElement('div');
            navContainer.id = 'chat-nav-container';
            const pathTop = 'M12 4l-6 6 1.41 1.41L12 6.83l4.59 4.58L18 10z M12 12l-6 6 1.41 1.41L12 14.83l4.59 4.58L18 18z';
            const pathUp = 'M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14z';
            const pathDown = 'M12 16l-6-6 1.41-1.41L12 13.17l4.59-4.58L18 10z';
            const pathBottom = 'M12 12l-6-6 1.41-1.41L12 9.17l4.59-4.58L18 6z M12 20l-6-6 1.41-1.41L12 17.17l4.59-4.58L18 14z';
            const btnTop = this.createButton('nav-top', 'Go to the first message (Shift + Alt + PgUp)', pathTop);
            const btnUp = this.createButton('nav-up', 'Go to the previous message (Alt + PgUp)', pathUp);
            const counterWrapper = document.createElement('div');
            counterWrapper.className = 'counter-wrapper';
            const counter = document.createElement('span');
            counter.id = 'chat-nav-counter';
            counter.title = 'Open navigation menu (Alt + M)';
            counter.tabIndex = 0;
            counter.setAttribute('role', 'button');
            counter.setAttribute('aria-haspopup', 'true');
            counter.setAttribute('aria-expanded', 'false');
            const currentNumSpan = document.createElement('span');
            currentNumSpan.id = 'chat-nav-current-num';
            counter.appendChild(currentNumSpan);
            const separatorSpan = document.createElement('span');
            separatorSpan.id = 'chat-nav-separator';
            separatorSpan.textContent = ' / ';
            counter.appendChild(separatorSpan);
            const totalNumSpan = document.createElement('span');
            totalNumSpan.id = 'chat-nav-total-num';
            counter.appendChild(totalNumSpan);
            const btnDown = this.createButton('nav-down', 'Go to the next message (Alt + PgDown)', pathDown);
            const btnBottom = this.createButton('nav-bottom', 'Go to the last message (Shift + Alt + PgDown)', pathBottom);
            let menuContainer = document.getElementById('chat-nav-menu-container');
            if (!menuContainer) {
                menuContainer = document.createElement('div');
                menuContainer.id = 'chat-nav-menu-container';
                menuContainer.tabIndex = -1;
                menuContainer.setAttribute('role', 'menu');
                document.body.appendChild(menuContainer);
            }
            counterWrapper.append(counter);
            navContainer.append(btnTop, btnUp, counterWrapper, btnDown, btnBottom);
            const parentContainer = targetNode.closest('footer');
            if (parentContainer && parentContainer.parentNode) {
                parentContainer.parentNode.insertBefore(navContainer, parentContainer);
                this.attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer);
                this.updateCounterDisplay();

                navContainer.addEventListener('mouseenter', () => {
                    this.isHoveringOnNav = true;
                    this.updateScrollPercentage();
                    this.showBadge();
                });
                navContainer.addEventListener('mouseleave', () => {
                    this.isHoveringOnNav = false;
                    this.hideBadge(1000);
                });
            }
        },

        createButton(id, title, pathData) {
            const button = document.createElement('button');
            button.id = id;
            button.className = 'chat-nav-button';
            button.title = title;
            button.setAttribute('aria-label', title);
            const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            svg.setAttribute('height', '24px');
            svg.setAttribute('viewBox', '0 0 24 24');
            svg.setAttribute('width', '24px');
            svg.setAttribute('fill', 'currentColor');
            const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
            path.setAttribute('d', pathData);
            svg.appendChild(path);
            button.appendChild(svg);
            return button;
        },

// Attaches event listeners and logic to the navigation UI buttons and menu.
        attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer) {
            const lastIndex = () => ChatController.allTurns.length - 1;

            ChatController.setupHoldableButton(btnTop, () => {
                ChatController.scrollToAbsoluteTop();
            });

            ChatController.setupHoldableButton(btnUp, async () => {
                const last = lastIndex();
                const scrollContainer = document.querySelector('ms-autoscroll-container');

                if (ChatController.currentIndex === last && last > -1 && scrollContainer) {
                    const currentTurn = ChatController.allTurns[last];
                    const isScrolledPastTop = scrollContainer.scrollTop > currentTurn.offsetTop + 10;
                    if (isScrolledPastTop) {
                        await ChatController.navigateToIndex(last, 'center');
                        return;
                    }
                }

                if (ChatController.currentIndex === 0) {
                    ChatController.scrollToAbsoluteTop();
                    return;
                }
                await ChatController.navigateToIndex(ChatController.currentIndex - 1, 'center');
            });

            ChatController.setupHoldableButton(btnDown, async () => {
                const last = lastIndex();
                if (ChatController.currentIndex < last) {
                    await ChatController.navigateToIndex(ChatController.currentIndex + 1, 'center');
                } else {
                    const scrollContainer = document.querySelector('ms-autoscroll-container');
                    if (!scrollContainer) return;
                    if (ChatController.isDownButtonAtEndToggle) {
                        await ChatController.navigateToIndex(last, 'center');
                        ChatController.isDownButtonAtEndToggle = false;
                    } else {
                        UIManager.hideBadge(0, true);
                        ChatController.isScrollingProgrammatically = true;
                        scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' });
                        setTimeout(() => {
                            ChatController.isScrollingProgrammatically = false;
                            UIManager.showBadge();
                            UIManager.updateScrollPercentage();
                            UIManager.hideBadge(3000);
                            ChatController.focusChatContainer();
                        }, 800);
                        ChatController.isDownButtonAtEndToggle = true;
                    }
                }
            });

            let ignoreNextBottomClick = false;
            const bottomNavAction = async () => {
                UIManager.hideBadge(0, true);
                const last = lastIndex();
                if (ChatController.currentIndex === last) {
                    const scrollContainer = document.querySelector('ms-autoscroll-container');
                    if (scrollContainer) {
                        const isSmooth = !ChatController.isBottomHoldActive;
                        ChatController.isScrollingProgrammatically = true;
                        scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: isSmooth ? 'smooth' : 'auto' });
                        setTimeout(() => {
                            ChatController.isScrollingProgrammatically = false;
                            UIManager.showBadge();
                            UIManager.updateScrollPercentage();
                            UIManager.hideBadge(3000);
                            ChatController.focusChatContainer();
                        }, isSmooth ? 800 : 50);
                    }
                } else {
                    await ChatController.navigateToIndex(last, 'center');
                }
            };
            btnBottom.addEventListener('mousedown', (e) => {
                if (ChatController.isBottomHoldActive) return;
                ChatController.bottomHoldTimeout = setTimeout(() => {
                    ChatController.isBottomHoldActive = true;
                    btnBottom.classList.add('auto-click-active');
                    ignoreNextBottomClick = true;
                    bottomNavAction();
                    ChatController.bottomHoldInterval = setInterval(bottomNavAction, 150);
                }, 750);
            });
            const stopBottomHoldDetector = () => { clearTimeout(ChatController.bottomHoldTimeout); };
            btnBottom.addEventListener('mouseup', stopBottomHoldDetector);
            btnBottom.addEventListener('mouseleave', stopBottomHoldDetector);
            btnBottom.addEventListener('click', (e) => {
                if (ignoreNextBottomClick) {
                    ignoreNextBottomClick = false;
                    return;
                }
                if (ChatController.isBottomHoldActive) {
                    ChatController.stopBottomHold();
                } else {
                    if (e.target.closest('#nav-bottom')) {
                        bottomNavAction();
                    }
                }
            });

            counter.addEventListener('click', (e) => { e.stopPropagation(); this.toggleNavMenu(); });
            counter.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    this.toggleNavMenu();
                } else if (e.key === 'Tab' && menuContainer.classList.contains('visible')) {
                    e.preventDefault();
                    const menuList = document.getElementById('chat-nav-menu');
                    const donateBtn = document.getElementById('chat-nav-donate-link');
                    if (e.shiftKey) {
                        if (donateBtn) donateBtn.focus();
                    } else {
                        if (menuList) menuList.focus();
                    }
                }
            });

            menuContainer.addEventListener('keydown', (e) => {
                if (e.key === 'Tab') {
                    e.preventDefault();
                    const menuList = document.getElementById('chat-nav-menu');
                    const loadBtn = document.getElementById('chat-nav-load-button');
                    const donateBtn = document.getElementById('chat-nav-donate-link');
                    const activeEl = document.activeElement;

                    if (e.shiftKey) {
                        if (activeEl === menuList) counter.focus();
                        else if (activeEl === loadBtn) menuList.focus();
                        else if (activeEl === donateBtn) loadBtn.focus();
                    } else {
                        if (activeEl === menuList) loadBtn.focus();
                        else if (activeEl === loadBtn) donateBtn.focus();
                        else if (activeEl === donateBtn) counter.focus();
                    }
                    return;
                }

                const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
                let newIndex = ChatController.menuFocusedIndex;
                let shouldUpdateFocus = true;

                switch (e.key) {
                    case 'ArrowDown': e.preventDefault(); if (items.length > 0) newIndex = (ChatController.menuFocusedIndex + 1) % items.length; break;
                    case 'ArrowUp': e.preventDefault(); if (items.length > 0) newIndex = (ChatController.menuFocusedIndex - 1 + items.length) % items.length; break;
                    case 'PageDown': e.preventDefault(); if (items.length > 0) newIndex = Math.min(items.length - 1, ChatController.menuFocusedIndex + ChatController.JUMP_DISTANCE); break;
                    case 'PageUp': e.preventDefault(); if (items.length > 0) newIndex = Math.max(0, ChatController.menuFocusedIndex - ChatController.JUMP_DISTANCE); break;
                    case 'Home': e.preventDefault(); if (items.length > 0) newIndex = 0; break;
                    case 'End': e.preventDefault(); if (items.length > 0) newIndex = items.length - 1; break;
                    case 'Enter':
                        e.preventDefault();
                        const activeEl = document.activeElement;
                        if (activeEl && activeEl.matches('button, a[href]') && menuContainer.contains(activeEl)) {
                            activeEl.click();
                        } else if (ChatController.menuFocusedIndex !== -1 && items[ChatController.menuFocusedIndex]) {
                            items[ChatController.menuFocusedIndex].click();
                        }
                        shouldUpdateFocus = false;
                        break;
                    case 'Escape': e.preventDefault(); this.toggleNavMenu(); shouldUpdateFocus = false; break;
                    default: shouldUpdateFocus = false; break;
                }
                if (shouldUpdateFocus && newIndex !== ChatController.menuFocusedIndex) { this.updateMenuFocus(items, newIndex); }
            });
        },

// Handles mouse movement over the highlighted turn to show/hide the badge.
        _handleTurnMouseMove(e) {
            const hotzoneWidth = 24;
            const rect = this.listenedTurnElement.getBoundingClientRect();
            const cursorX = e.clientX;
            if (cursorX >= rect.left && cursorX <= rect.left + hotzoneWidth ||
                cursorX >= rect.right - hotzoneWidth && cursorX <= rect.right) {
                this.showBadge();
                this.updateScrollPercentage();
            } else {
                this.hideBadge(1000);
            }
        },

        _handleTurnMouseLeave() {
            this.hideBadge(1000);
        },

// Updates the visual highlight, ensuring only one turn is highlighted at a time.
        updateHighlight(oldIndex, newIndex) {
            if (oldIndex > -1 && oldIndex < ChatController.allTurns.length) {
                const oldTurn = ChatController.allTurns[oldIndex];
                const oldTurnContainer = oldTurn.querySelector('.chat-turn-container');
                if (oldTurnContainer) {
                    oldTurnContainer.classList.remove('prompt-turn-highlight', 'response-turn-highlight');
                }
            }

            if (this.listenedTurnElement) {
                this.listenedTurnElement.removeEventListener('mousemove', this._handleTurnMouseMove);
                this.listenedTurnElement.removeEventListener('mouseleave', this._handleTurnMouseLeave);
                this.listenedTurnElement = null;
            }
            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater) return;

            if (newIndex > -1 && newIndex < ChatController.allTurns.length) {
                const newTurn = ChatController.allTurns[newIndex];
                const isPrompt = ChatController.isUserPrompt(newTurn);
                const newContainer = newTurn.querySelector('.chat-turn-container');
                if (newContainer) {
                    newContainer.classList.add(isPrompt ? 'prompt-turn-highlight' : 'response-turn-highlight');
                }
                const badgeIndex = document.getElementById('quicknav-badge-index');
                const badgeClass = isPrompt ? 'prompt-badge-bg' : 'response-badge-bg';
                if (badgeIndex) {
                    badgeIndex.textContent = newIndex + 1;
                }
                floater.className = badgeClass;
                this.listenedTurnElement = newTurn;
                this.listenedTurnElement.addEventListener('mousemove', this._handleTurnMouseMove);
                this.listenedTurnElement.addEventListener('mouseleave', this._handleTurnMouseLeave);
            }
        },

        updateCounterDisplay() {
            let currentNumSpan = document.getElementById('chat-nav-current-num');
            let totalNumSpan = document.getElementById('chat-nav-total-num');
            if (!currentNumSpan || !totalNumSpan) return;
            const current = ChatController.currentIndex > -1 ? ChatController.currentIndex + 1 : '-';
            const total = ChatController.allTurns.length;
            currentNumSpan.textContent = current;
            totalNumSpan.textContent = total;
            currentNumSpan.classList.remove('chat-nav-current-grey', 'chat-nav-current-blue');
            if (ChatController.currentIndex === total - 1 && total > 0) {
                currentNumSpan.classList.add('chat-nav-current-blue');
            } else {
                currentNumSpan.classList.add('chat-nav-current-grey');
            }
        },

// Toggles the visibility and state of the main navigation menu.
        toggleNavMenu() {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            const counter = document.getElementById('chat-nav-counter');
            if (!menuContainer || !counter) return;

            const isVisible = menuContainer.classList.contains('visible');
            if (isVisible) {
                menuContainer.classList.remove('visible');
                const menuList = document.getElementById('chat-nav-menu');
                if (menuList) menuList.tabIndex = -1;
                counter.setAttribute('aria-expanded', 'false');
                document.removeEventListener('click', this.closeNavMenu, true);
                ChatController.stopDynamicMenuLoading();
                ChatController.focusChatContainer();
            } else {
                this.populateNavMenu();
                const newMenuList = document.getElementById('chat-nav-menu');
                if (newMenuList) newMenuList.tabIndex = 0;

                const chatContainer = document.querySelector('ms-chunk-editor');
                if (chatContainer) {
                    const chatWidth = chatContainer.clientWidth;
                    const finalWidth = Math.max(300, Math.min(chatWidth, 800));
                    menuContainer.style.width = `${finalWidth}px`;
                }
                const counterRect = counter.getBoundingClientRect();
                menuContainer.style.bottom = `${window.innerHeight - counterRect.top + 8}px`;
                menuContainer.style.left = `${counterRect.left + (counterRect.width / 2)}px`;
                menuContainer.style.transform = 'translateX(-50%)';

                const availableSpace = counterRect.top - 18;
                menuContainer.style.maxHeight = `${availableSpace}px`;

                this._proactivelySyncMenuScroll();
                menuContainer.classList.add('visible');
                counter.setAttribute('aria-expanded', 'true');

                const items = newMenuList ? newMenuList.querySelectorAll('.chat-nav-menu-item') : [];
                const initialFocusIndex = ChatController.currentIndex > -1 ? ChatController.currentIndex : 0;
                this.updateMenuFocus(items, initialFocusIndex, false);

                if (newMenuList) {
                    newMenuList.focus();
                } else {
                    menuContainer.focus();
                }

                setTimeout(() => document.addEventListener('click', this.closeNavMenu, true), 0);
            }
        },

// Handles clicks outside the menu to close it.
        closeNavMenu(e) {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            const counter = document.getElementById('chat-nav-counter');
            if (menuContainer && counter && !menuContainer.contains(e.target) && !counter.contains(e.target) && menuContainer.classList.contains('visible')) {
                this.toggleNavMenu();
            }
        },

// Proactively sets the scroll position of the (possibly hidden) nav menu.
        _proactivelySyncMenuScroll() {
            const menuList = document.getElementById('chat-nav-menu');
            if (!menuList) return;

            const items = menuList.querySelectorAll('.chat-nav-menu-item');
            const targetIndex = ChatController.currentIndex;

            if (targetIndex < 0 || targetIndex >= items.length) return;

            const focusedItem = items[targetIndex];
            if (focusedItem) {
                menuList.style.scrollBehavior = 'auto';
                menuList.scrollTop = focusedItem.offsetTop - (menuList.clientHeight / 2) + (focusedItem.clientHeight / 2);
                menuList.style.scrollBehavior = '';
            }
        },

// Updates the text content of a single menu item if the menu is visible.
        updateMenuItemContent(index) {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            if (!menuContainer || !menuContainer.classList.contains('visible')) return;

            const menuList = document.getElementById('chat-nav-menu');
            if (!menuList) return;

            const menuItem = menuList.children[index];
            const turn = ChatController.allTurns[index];
            if (!menuItem || !turn || !turn.cachedContent) return;

            const textSpan = menuItem.querySelector('.menu-item-text');
            if (textSpan) {
                const { display, full } = turn.cachedContent;
                const truncatedText = (display.length > 200) ? display.substring(0, 197) + '...' : display;
                if (textSpan.textContent !== truncatedText) {
                    textSpan.textContent = truncatedText;
                }
                menuItem.dataset.tooltip = full.replace(/\s+/g, ' ');
            }
        },

        updateMenuFocus(items, newIndex, shouldScroll = true) {
            if (!items || items.length === 0 || newIndex < 0 || newIndex >= items.length) return;
            if (ChatController.menuFocusedIndex > -1 && ChatController.menuFocusedIndex < items.length) {
                items[ChatController.menuFocusedIndex].classList.remove('menu-item-focused');
            }
            items[newIndex].classList.add('menu-item-focused');
            if (shouldScroll) {
                const menuList = document.getElementById('chat-nav-menu');
                const focusedItem = items[newIndex];
                if (menuList && focusedItem) {
                    const itemRect = focusedItem.getBoundingClientRect();
                    const menuRect = menuList.getBoundingClientRect();
                    if (itemRect.bottom > menuRect.bottom) {
                        menuList.scrollTop += itemRect.bottom - menuRect.bottom;
                    } else if (itemRect.top < menuRect.top) {
                        menuList.scrollTop -= menuRect.top - itemRect.top;
                    }
                }
            }
            ChatController.menuFocusedIndex = newIndex;
        },

// Extracts text using a hybrid approach: DOM is priority, scrollbar is fallback.
        getTextFromTurn(turn, fromDOMOnly = false) {
            const contentContainer = turn.querySelector('.turn-content');
            if (contentContainer) {
                const clonedContainer = contentContainer.cloneNode(true);
                clonedContainer.querySelectorAll('ms-code-block').forEach(codeBlockElement => {
                    const codeContent = codeBlockElement.querySelector('pre code');
                    if (codeContent) {
                        const pre = document.createElement('pre');
                        pre.textContent = ` [Code Block] `;
                        codeBlockElement.parentNode.replaceChild(pre, codeBlockElement);
                    } else { codeBlockElement.remove(); }
                });
                clonedContainer.querySelectorAll('.author-label, .turn-separator, ms-thought-chunk').forEach(el => el.remove());
                const text = clonedContainer.textContent?.trim().replace(/\s+/g, ' ');

                if (text) {
                    return { display: text, full: text, source: 'dom' };
                }
            }

            if (!fromDOMOnly) {
                const turnId = turn.id;
                if (turnId) {
                    const scrollbarButton = document.getElementById(`scrollbar-item-${turnId.replace('turn-', '')}`);
                    if (scrollbarButton && scrollbarButton.getAttribute('aria-label')) {
                        const labelText = scrollbarButton.getAttribute('aria-label');
                        return { display: labelText, full: labelText, source: 'scrollbar' };
                    }
                }
            }

            return { display: '...', full: 'Could not extract content.', source: 'fallback' };
        },

// Populates the navigation menu, performing an initial cache fill on first run.
        populateNavMenu() {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            if (!menuContainer) return;
            while (menuContainer.firstChild) { menuContainer.removeChild(menuContainer.firstChild); }
            const header = document.createElement('div');
            header.className = 'chat-nav-menu-header';
            const menuList = document.createElement('ul');
            menuList.id = 'chat-nav-menu';
            menuList.tabIndex = -1;

            ChatController.allTurns.forEach((turn, index) => {
                let displayContent;
                if (turn.cachedContent && !turn.isFallbackContent) {
                    displayContent = turn.cachedContent;
                } else {
                    displayContent = this.getTextFromTurn(turn);

                    const isStreaming = !!turn.querySelector('loading-indicator');

                    if (isStreaming) {
                        turn.cachedContent = null;
                        turn.isFallbackContent = true;
                    } else {
                        turn.cachedContent = displayContent;
                        turn.isFallbackContent = displayContent.source === 'fallback';
                    }
                }

                const { display, full } = displayContent;
                const truncatedText = (display.length > 200) ? display.substring(0, 197) + '...' : display;
                const item = document.createElement('li');
                item.className = 'chat-nav-menu-item';
                item.setAttribute('role', 'menuitem');
                const isPrompt = ChatController.isUserPrompt(turn);
                item.classList.add(isPrompt ? 'prompt-item-bg' : 'response-item-bg');
                const numberSpan = document.createElement('span');
                numberSpan.className = `menu-item-number ${isPrompt ? 'prompt-number-color' : 'response-number-color'}`;
                numberSpan.textContent = `${index + 1}.`;
                const textSpan = document.createElement('span');
                textSpan.className = 'menu-item-text';
                textSpan.textContent = truncatedText;
                item.append(numberSpan, textSpan);
                item.dataset.tooltip = full.replace(/\s+/g, ' ');

                item.addEventListener('click', () => {
                    this.toggleNavMenu();
                    ChatController.navigateToIndex(index);
                });
                menuList.appendChild(item);
            });

            menuList.addEventListener('wheel', (e) => {
                const list = e.currentTarget;
                const isScrollable = list.scrollHeight > list.clientHeight;
                const isScrollingUp = e.deltaY < 0;
                const isScrollingDown = e.deltaY > 0;

                if (!isScrollable) {
                    e.preventDefault();
                    return;
                }

                const isAtTop = list.scrollTop === 0;
                const isAtBottom = Math.ceil(list.scrollTop + list.clientHeight) >= list.scrollHeight;

                if ((isAtTop && isScrollingUp) || (isAtBottom && isScrollingDown)) {
                    e.preventDefault();
                }
            });

            let tooltipTarget = null;
            menuList.addEventListener('mousemove', e => {
                const currentTarget = e.target.closest('.chat-nav-menu-item');
                if (currentTarget && currentTarget === tooltipTarget && this.customTooltip.style.opacity === '1') {
                    this.positionTooltip(e);
                    return;
                }
                if (currentTarget !== tooltipTarget) {
                    clearTimeout(this.tooltipTimeout);
                    this.customTooltip.style.opacity = '0';
                    tooltipTarget = currentTarget;
                    if (tooltipTarget) {
                        this.tooltipTimeout = setTimeout(() => {
                            if (!tooltipTarget) return;
                            this.customTooltip.textContent = tooltipTarget.dataset.tooltip;
                            if (tooltipTarget.classList.contains('response-item-bg')) {
                                const isDarkMode = document.body.classList.contains('dark-theme');
                                this.customTooltip.style.color = isDarkMode ? 'var(--ms-primary, #8ab4f8)' : '#174ea6';
                            } else {
                                this.customTooltip.style.color = '';
                            }
                            this.customTooltip.style.opacity = '1';
                            requestAnimationFrame(() => this.positionTooltip(e));
                        }, 500);
                    }
                }
            });
            menuList.addEventListener('mouseleave', () => {
                clearTimeout(this.tooltipTimeout);
                this.customTooltip.style.opacity = '0';
                tooltipTarget = null;
            });

            const leftContainer = document.createElement('div');
            leftContainer.className = 'header-controls left';
            const loadButton = document.createElement('button');
            loadButton.id = 'chat-nav-load-button';
            loadButton.className = 'header-button';
            loadButton.textContent = 'Load All';
            loadButton.title = 'Load full text for all messages';
            loadButton.addEventListener('click', (e) => {
                e.stopPropagation();
                ChatController.startDynamicMenuLoading();
            });
            const statusIndicator = document.createElement('span');
            statusIndicator.id = 'chat-nav-loader-status';
            leftContainer.append(loadButton, statusIndicator);
            const titleElement = document.createElement('div');
            titleElement.className = 'quicknav-title google-text-animated';
            titleElement.textContent = 'QuickNav for Google AI Studio';
            const rightContainer = document.createElement('div');
            rightContainer.className = 'header-controls right';
            const donateButton = document.createElement('a');
            donateButton.id = 'chat-nav-donate-link';
            donateButton.href = 'https://nowpayments.io/donation/axl_script';
            donateButton.target = '_blank';
            donateButton.rel = 'noopener noreferrer';
            donateButton.className = 'header-button';
            donateButton.title = 'Support the developer';
            const donateText = document.createElement('span');
            donateText.className = 'donate-button-animated';
            donateText.textContent = 'Donate';
            donateButton.appendChild(donateText);
            donateButton.addEventListener('click', (e) => e.stopPropagation());
            rightContainer.append(donateButton);
            header.append(leftContainer, titleElement, rightContainer);
            menuContainer.appendChild(header);
            menuContainer.appendChild(menuList);
        },

// Updates the badge's position and content, ensuring it stays within all UI boundaries.
        updateScrollPercentage() {
            const floater = document.getElementById('quicknav-badge-floater');
            const percentageBadge = document.getElementById('quicknav-badge-percentage');
            const scrollContainer = document.querySelector('ms-autoscroll-container');

            if (!floater || !percentageBadge || !scrollContainer || ChatController.currentIndex < 0) {
                if (floater) this.hideBadge(0, true);
                return;
            }

            const currentTurn = ChatController.allTurns[ChatController.currentIndex];
            if (!currentTurn) {
                this.hideBadge(0, true);
                return;
            }

            const rect = currentTurn.getBoundingClientRect();
            const scrollContainerRect = scrollContainer.getBoundingClientRect();
            const turnHeight = rect.height;
            const viewportHeight = window.innerHeight;
            const MIN_VISIBLE_HEIGHT = 40;

            const visibleHeight = Math.max(0, Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0));
            const isElementVisible = visibleHeight >= MIN_VISIBLE_HEIGHT;

            if (floater.style.opacity === '1' && !isElementVisible) {
                this.hideBadge(0, true);
                return;
            }
            if (!isElementVisible || turnHeight <= 0) {
                return;
            }

            const floaterHeight = floater.offsetHeight || 44;

            // Redefined ideal position: Center of the *visible portion* of the message turn.
            const visibleTurnTop = Math.max(rect.top, scrollContainerRect.top);
            const visibleTurnBottom = Math.min(rect.bottom, scrollContainerRect.bottom);
            const idealTop = (visibleTurnTop + visibleTurnBottom) / 2 - (floaterHeight / 2);

            const toolbarBottom = this.toolbarElement ? this.toolbarElement.getBoundingClientRect().bottom : 0;
            const footerTop = this.footerElement ? this.footerElement.getBoundingClientRect().top : viewportHeight;
            const navContainerTop = this.navContainerElement ? this.navContainerElement.getBoundingClientRect().top : viewportHeight;

            // These boundaries correctly clamp the position within the message AND other UI elements.
            const upperBound = Math.max(scrollContainerRect.top + 4, rect.top, toolbarBottom + 4);
            const lowerBound = Math.min(scrollContainerRect.bottom, rect.bottom, footerTop, navContainerTop) - floaterHeight - 4;

            let finalTop = Math.max(upperBound, Math.min(idealTop, lowerBound));
            floater.style.top = `${finalTop}px`;
            floater.style.left = `${rect.right - (floater.offsetWidth / 2)}px`;

            const viewportCenterY = viewportHeight / 2;
            const distanceScrolled = viewportCenterY - rect.top;
            const percentage = (distanceScrolled / turnHeight) * 100;
            const clampedPercentage = Math.max(0, Math.min(percentage, 100));
            percentageBadge.textContent = `${Math.round(clampedPercentage)}%`;
        },

        positionTooltip(e) {
            const tooltip = this.customTooltip;
            if (!tooltip) return;
            const winWidth = window.innerWidth;
            const winHeight = window.innerHeight;
            const tipHeight = tooltip.offsetHeight;
            const tipWidth = tooltip.offsetWidth;
            const margin = 10;
            let top;
            if (tipHeight >= winHeight) {
                top = 0;
            } else {
                top = e.clientY + 15;
                if (top + tipHeight + margin > winHeight) {
                    top = winHeight - tipHeight - margin;
                }
            }
            let left = e.clientX + 100;
            if (left + tipWidth + margin > winWidth) {
                left = winWidth - tipWidth - margin;
            }
            left = Math.max(margin, left);
            tooltip.style.top = `${top}px`;
            tooltip.style.left = `${left}px`;
        }
    };

    UIManager.closeNavMenu = UIManager.closeNavMenu.bind(UIManager);

// --- MODULE: Code Block Navigator ---
// Finds code blocks and injects or updates intra-message navigation controls.
    const CodeBlockNavigator = {
        processTurns(allTurns) {
            if (!allTurns) return;
            allTurns.forEach(turn => {
                const codeBlocksInTurn = Array.from(turn.querySelectorAll('ms-code-block'));
                if (codeBlocksInTurn.length === 0) return;

                codeBlocksInTurn.forEach((block, index) => {
                    const header = block.querySelector('mat-expansion-panel-header .mat-content');
                    const actionsContainer = header ? header.querySelector('.actions-container') : null;
                    if (!header || !actionsContainer) return;

                    let navContainer = header.querySelector('.code-block-nav-container');

                    if (!navContainer) {
                        navContainer = document.createElement('div');
                        navContainer.className = 'code-block-nav-container';

                        const pathUp = 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z';
                        const pathDown = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6z';

                        const btnUp = this._createNavButton(pathUp);
                        const counter = document.createElement('div');
                        counter.className = 'code-nav-counter';
                        const btnDown = this._createNavButton(pathDown);

                        btnUp.addEventListener('click', (e) => this._handleNavClick(e));
                        btnDown.addEventListener('click', (e) => this._handleNavClick(e));

                        navContainer.append(btnUp, counter, btnDown);
                        actionsContainer.appendChild(navContainer);
                    }

                    const [btnUp, counter, btnDown] = navContainer.children;
                    const totalBlocks = codeBlocksInTurn.length;
                    const currentBlockNum = index + 1;
                    const isFirst = index === 0;
                    const isLast = index === totalBlocks - 1;

                    counter.textContent = `${currentBlockNum} / ${totalBlocks}`;
                    counter.title = `Code block ${currentBlockNum} of ${totalBlocks}`;

                    if (isFirst) {
                        btnUp.dataset.navTarget = 'header';
                        btnUp.title = `Scroll to the header of this block (${currentBlockNum}/${totalBlocks})`;
                    } else {
                        btnUp.dataset.navTarget = 'previous';
                        btnUp.title = `Go to previous block (${currentBlockNum - 1}/${totalBlocks})`;
                    }

                    if (isLast) {
                        btnDown.dataset.navTarget = 'footer';
                        btnDown.title = `Scroll to the footer of this block (${currentBlockNum}/${totalBlocks})`;
                    } else {
                        btnDown.dataset.navTarget = 'next';
                        btnDown.title = `Go to next block (${currentBlockNum + 1}/${totalBlocks})`;
                    }
                });
            });
        },

        _createNavButton(pathData) {
            const button = document.createElement('button');
            button.className = 'code-nav-button';
            const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            svg.setAttribute('height', '16px');
            svg.setAttribute('viewBox', '0 0 24 24');
            svg.setAttribute('width', '16px');
            svg.setAttribute('fill', 'currentColor');
            const path = document.createElementNS("http://www.w3.org/2000/svg", 'path');
            path.setAttribute('d', pathData);
            svg.appendChild(path);
            button.appendChild(svg);
            return button;
        },

        _handleNavClick(event) {
            event.stopPropagation();
            event.preventDefault();

            const button = event.currentTarget;
            const navTarget = button.dataset.navTarget;
            const currentCodeBlock = button.closest('ms-code-block');
            if (!currentCodeBlock) return;

            const parentTurn = currentCodeBlock.closest('ms-chat-turn');
            if (!parentTurn) return;

            const turnCodeBlocks = Array.from(parentTurn.querySelectorAll('ms-code-block'));
            const currentIndex = turnCodeBlocks.indexOf(currentCodeBlock);

            let targetBlock = null;

            switch (navTarget) {
                case 'header':
                    this._scrollToElement(currentCodeBlock, 'header');
                    break;
                case 'footer':
                    this._scrollToElement(currentCodeBlock, 'footer');
                    break;
                case 'previous':
                    if (currentIndex > 0) {
                        targetBlock = turnCodeBlocks[currentIndex - 1];
                        this._scrollToElement(targetBlock, 'header');
                    }
                    break;
                case 'next':
                    if (currentIndex < turnCodeBlocks.length - 1) {
                        targetBlock = turnCodeBlocks[currentIndex + 1];
                        this._scrollToElement(targetBlock, 'header');
                    }
                    break;
            }
        },

        _scrollToElement(element, position = 'header') {
            if (!element) return;
            ChatController.isScrollingProgrammatically = true;

            const anchor = document.createElement('div');
            let scrollOptions = { behavior: 'smooth', block: 'center' };
            let targetNodeForAnchor;

            if (position === 'header') {
                targetNodeForAnchor = element.querySelector('mat-expansion-panel-header');
                if (targetNodeForAnchor) {
                    anchor.style.cssText = 'position: absolute; height: 0; width: 0; margin-top: -30px;';
                    targetNodeForAnchor.parentNode.insertBefore(anchor, targetNodeForAnchor);
                }
            } else {
                targetNodeForAnchor = element;
                anchor.style.cssText = 'display: block; height: 1px;';
                targetNodeForAnchor.appendChild(anchor);
                scrollOptions.block = 'end';
            }

            if (targetNodeForAnchor) {
                anchor.scrollIntoView(scrollOptions);
            }

            setTimeout(() => {
                anchor.remove();
                ChatController.isScrollingProgrammatically = false;
            }, 700);
        }
    };

    // --- MODULE: Hotkeys Manager ---
// Handles global keyboard shortcuts for navigation.
    const HotkeysManager = {
        pressedKeys: new Set(),
        keyMap: {
            'Alt_PageUp': '#nav-up',
            'Alt_PageDown': '#nav-down',
            'Shift_Alt_PageUp': '#nav-top',
            'Shift_Alt_PageDown': '#nav-bottom',
            'Alt_KeyM': '#chat-nav-counter'
        },

        init() {
            document.addEventListener('keydown', this.handleKeyDown.bind(this));
            document.addEventListener('keyup', this.handleKeyUp.bind(this));
            window.addEventListener('blur', this.handleBlur.bind(this));
        },

        getKeyCombination(e) {
            let key = '';
            if (e.shiftKey) key += 'Shift_';
            if (e.altKey) key += 'Alt_';
            key += e.code;
            return key;
        },

        handleKeyDown(e) {
            if (e.altKey && e.code === 'KeyP') {
                const promptInput = document.querySelector('ms-prompt-input-wrapper textarea');
                if (promptInput && document.activeElement !== promptInput) {
                    e.preventDefault();
                    e.stopPropagation();
                    ChatController.focusPromptInput();
                    return;
                }
            }
            const isTypingArea = e.target.isContentEditable || e.target.closest('[contenteditable="true"]') || ['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName);
            if (e.repeat || (!e.altKey && isTypingArea)) {
                return;
            }
            const combination = this.getKeyCombination(e);
            const targetSelector = this.keyMap[combination];
            if (targetSelector) {
                e.preventDefault();
                e.stopPropagation();
                if (!this.pressedKeys.has(combination)) {
                    this.pressedKeys.add(combination);
                    const targetElement = document.querySelector(targetSelector);
                    if (targetElement) {
                        targetElement.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true, view: window, buttons: 1 }));
                    }
                }
            }
        },

        handleKeyUp(e) {
            const releasedCombos = [];
            this.pressedKeys.forEach(combo => {
                const parts = combo.split('_');
                const mainKey = parts[parts.length - 1];
                let isReleased = false;
                if (mainKey === e.code) {
                    isReleased = true;
                } else if ((parts.includes('Shift') && (e.code === 'ShiftLeft' || e.code === 'ShiftRight')) || (parts.includes('Alt') && (e.code === 'AltLeft' || e.code === 'AltRight'))) {
                    const requiredAlt = parts.includes('Alt');
                    const requiredShift = parts.includes('Shift');
                    if ((requiredAlt && !e.altKey) || (requiredShift && !e.shiftKey)) {
                        isReleased = true;
                    }
                }
                if (isReleased) {
                    releasedCombos.push(combo);
                }
            });

            if (releasedCombos.length > 0) {
                e.preventDefault();
                e.stopPropagation();
                releasedCombos.forEach(combo => {
                    this.pressedKeys.delete(combo);
                    const targetSelector = this.keyMap[combo];
                    if (targetSelector) {
                        const targetElement = document.querySelector(targetSelector);
                        if (targetElement) {
                            targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, buttons: 0 }));
                            targetElement.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window, buttons: 1 }));
                        }
                    }
                });
            }
        },

        handleBlur() {
            this.pressedKeys.forEach(combo => {
                const targetSelector = this.keyMap[combo];
                if (targetSelector) {
                    const targetElement = document.querySelector(targetSelector);
                    if (targetElement) {
                        targetElement.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true, view: window, buttons: 0 }));
                    }
                }
            });
            this.pressedKeys.clear();
        }
    };

    // --- MODULE: DOM Observer ---
// Watches for the main chat container to appear or disappear.
    const DOMObserver = {
        observer: null,
        isChatActive: false,
        onChatReady: null,
        onChatDestroyed: null,
        CHAT_CONTAINER_SELECTOR: 'div.chat-container',
        UI_ANCHOR_SELECTOR: 'ms-prompt-input-wrapper',

        start(onReady, onDestroyed) {
            this.onChatReady = onReady;
            this.onChatDestroyed = onDestroyed;

            this.observer = new MutationObserver(this.handleMutations.bind(this));
            this.observer.observe(document.body, { childList: true, subtree: true });
            this.checkForInitialChat();
        },

        handleMutations(mutationsList) {
            let chatAppeared = false;
            let chatDisappeared = false;

            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches(this.CHAT_CONTAINER_SELECTOR) || node.querySelector(this.CHAT_CONTAINER_SELECTOR)) {
                                chatAppeared = true;
                            }
                        }
                    }
                    for (const node of mutation.removedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches(this.CHAT_CONTAINER_SELECTOR) || node.querySelector(this.CHAT_CONTAINER_SELECTOR)) {
                                chatDisappeared = true;
                            }
                        }
                    }
                }
            }

            if (chatAppeared && !this.isChatActive) {
                const chatContainer = document.querySelector(this.CHAT_CONTAINER_SELECTOR);
                const uiAnchor = document.querySelector(this.UI_ANCHOR_SELECTOR);
                if (chatContainer && uiAnchor) {
                    this.isChatActive = true;
                    this.onChatReady(chatContainer, uiAnchor);
                }
            } else if (chatDisappeared && this.isChatActive) {
                 if (!document.querySelector(this.CHAT_CONTAINER_SELECTOR)) {
                    this.isChatActive = false;
                    this.onChatDestroyed();
                }
            }
        },

        checkForInitialChat() {
            const chatContainer = document.querySelector(this.CHAT_CONTAINER_SELECTOR);
            const uiAnchor = document.querySelector(this.UI_ANCHOR_SELECTOR);
            if (chatContainer && uiAnchor && !this.isChatActive) {
                this.isChatActive = true;
                this.onChatReady(chatContainer, uiAnchor);
            }
        }
    };

    // --- MAIN APP ORCHESTRATOR ---
    // Initializes and coordinates all modules.
    const QuickNavApp = {
        init() {
            DOMObserver.start(
                this.handleChatReady.bind(this),
                this.handleChatDestroyed.bind(this)
            );
            HotkeysManager.init();
        },

        handleChatReady(chatContainer, uiAnchor) {
            UIManager.create(uiAnchor);
            ChatController.init(chatContainer);
        },

        handleChatDestroyed() {
            UIManager.destroy();
            ChatController.destroy();
        }
    };

    QuickNavApp.init();

})();