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. This script operates entirely locally in your browser, does not collect any personal data, and makes no requests to external servers.

// ==UserScript==
// @name         QuickNav for Google AI Studio
// @namespace    http://tampermonkey.net/
// @version      18.15
// @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. 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/embeds/donation-widget?api_key=0fe4e67c-64aa-4a74-b2d2-a91608b1ccc6
// @match        https://aistudio.google.com/*
// @grant        none
// @license      MIT
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const ChatNavigator = {
        allTurns: [],
        currentIndex: -1,
        menuFocusedIndex: -1,
        isDownButtonAtEndToggle: false,
        JUMP_DISTANCE: 5,
        scrollSyncTimeout: null,
        isScrollingProgrammatically: false,
        isQueueProcessing: false,
        isUnstickingFromBottom: false,
        originalScrollTop: 0,
        originalCurrentIndex: -1,
        loadingQueue: [],
        totalToLoad: 0,
        mainLoopInterval: null,
        activeObserver: null,
        observedElement: null,
        holdTimeout: null,
        holdInterval: null,

        init() {
            this.injectStyles();
            this.setupScrollListener();
            if (document.getElementById('quicknav-badge-floater')) return;
            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 (this.mainLoopInterval) clearInterval(this.mainLoopInterval);
            this.mainLoopInterval = setInterval(() => this.mainLoop(), 750);
        },

        mainLoop() {
            const chatContainer = document.querySelector('ms-autoscroll-container');

            if (chatContainer) {
                if (!this.activeObserver || this.observedElement !== chatContainer) {
                    this.cleanupChatObserver();
                    this.initializeChatObserver(chatContainer);
                    this.buildTurnIndex();
                }
            } else {
                if (this.activeObserver) {
                    this.cleanupChatObserver();
                    this.allTurns = [];
                    this.currentIndex = -1;
                }
            }
            this.ensureUIState();
        },

        ensureUIState() {
            const targetNode = document.querySelector('ms-prompt-input-wrapper');
            const uiExists = document.getElementById('chat-nav-container');

            if (targetNode && !uiExists) {
                this.createAndInjectUI(targetNode);
            } else if (!targetNode && uiExists) {
                uiExists.remove();
                const menu = document.getElementById('chat-nav-menu-container');
                if (menu) menu.remove();
            }
        },

        initializeChatObserver(container) {
            this.observedElement = container;
            this.activeObserver = new MutationObserver(() => {
                this.buildTurnIndex();
            });
            this.activeObserver.observe(container, { childList: true, subtree: true });
        },

        cleanupChatObserver() {
            if (this.activeObserver) {
                this.activeObserver.disconnect();
            }
            this.activeObserver = null;
            this.observedElement = null;
        },

        getTurnType(turnElement) {
            const turnContainer = turnElement.querySelector('.chat-turn-container');
            if (!turnContainer) return 'unknown';
            if (turnContainer.classList.contains('user')) return 'user_prompt';
            if (turnContainer.classList.contains('model')) {
                return turnElement.querySelector('ms-thought-chunk') ? 'model_thought' : 'model_response';
            }
            return 'unknown';
        },

        buildTurnIndex() {
            const allFoundTurns = Array.from(document.querySelectorAll('ms-chat-turn'));
            const freshTurns = allFoundTurns.filter(turn => {
                const type = this.getTurnType(turn);
                return type === 'user_prompt' || type === 'model_response';
            });

            if (freshTurns.length !== this.allTurns.length || !this.arraysEqual(this.allTurns, freshTurns)) {
                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;
                    }
                });
                this.updateCounterDisplay();
                if (this.currentIndex >= this.allTurns.length) this.synchronizeCurrentIndexFromView();
            } else {
                this.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;
        },

        setupScrollListener() {
            document.addEventListener('scroll', () => {
                if (this.isScrollingProgrammatically || this.isQueueProcessing || this.isUnstickingFromBottom) return;
                this.updateScrollPercentage();
                clearTimeout(this.scrollSyncTimeout);
                this.scrollSyncTimeout = setTimeout(() => this.synchronizeCurrentIndexFromView(), 150);
            }, true);
        },

        injectStyles() {
            if (document.getElementById('chat-nav-styles')) return;
            const styleSheet = document.createElement("style");
            styleSheet.id = 'chat-nav-styles';
            styleSheet.textContent = `
                #chat-nav-container { display: flex; justify-content: center; align-items: center; gap: 12px; margin: 2px auto; width: 100%; box-sizing: border-box; }
                .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: 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: #8ab4f8;
                }

                #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); }

                #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, .response-turn-highlight { position: relative; border-radius: 12px; }
                .prompt-turn-highlight::after, .response-turn-highlight::after { content: ""; display: table; clear: both; }
                .prompt-turn-highlight { box-shadow: 0 0 0 1px #9aa0a6 !important; }
                .response-turn-highlight { box-shadow: 0 0 0 1px #8ab4f8 !important; }
                #quicknav-badge-floater { position: fixed; z-index: 1000; visibility: hidden; display: flex; flex-direction: column; align-items: center; pointer-events: none; padding: 4px 7px; border-radius: 8px; font-family: 'Google Sans', sans-serif; }
                #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: none; flex-direction: column; position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%); margin-bottom: 8px; background-color: #202124; border: 2px solid #8ab4f8; border-radius: 12px; box-shadow: 0 4px 12px rgba(0,0,0,0.25); max-height: 90vh; z-index: 10000; width: clamp(300px, 60vw, 800px); pointer-events: auto; box-sizing: border-box; }
                #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; background-color: #191919; }
                .chat-nav-menu-item { display: flex; align-items: center; color: #e8eaed; 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; background-color: #202124; }
                .chat-nav-menu-item:hover { background-color: #3c4043; }
                .chat-nav-menu-item.menu-item-focused { background-color: #5f6368; }
                .menu-item-number { font-weight: 500; margin-right: 8px; flex-shrink: 0; }
                .prompt-number-color { color: #9aa0a6; }
                .response-number-color { color: #8ab4f8; }
                .menu-item-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
                .prompt-item-bg { border-left: 3px solid #9aa0a6; margin-left: 32px; }
                .response-item-bg { border-left: 3px solid #8ab4f8; border-bottom: 1px solid #8ab4f8; }
                .chat-nav-menu-header { flex-shrink: 0; background-color: #202124; z-index: 1; display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; border-bottom: 1px solid #8ab4f8; border-radius: 10px 10px 0 0; }
                .header-button { font-family: 'Google Sans', sans-serif; text-decoration: none; font-size: 12px; color: #e8eaed; background-color: #3c4043; padding: 4px 10px; border-radius: 16px; transition: background-color 0.15s ease-in-out, opacity 0.15s ease-in-out; border: none; cursor: pointer; }
                .header-button:hover:not(:disabled) { background-color: #5f6368; }
                .header-button:disabled { opacity: 0.5; cursor: not-allowed; }
                #chat-nav-menu::-webkit-scrollbar { width: 8px; }
                #chat-nav-menu::-webkit-scrollbar-track { background: #202124; }
                #chat-nav-menu::-webkit-scrollbar-thumb { background-color: #5f6368; border-radius: 4px; }
                #chat-nav-menu::-webkit-scrollbar-thumb:hover { background-color: #9aa0a6; }
                #chat-nav-loader-status { font-family: 'Google Sans', sans-serif; font-size: 12px; color: #9aa0a6; padding: 4px 10px; }
            `;
            document.head.appendChild(styleSheet);
        },

        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 first message (Home)', pathTop);
            const btnUp = this.createButton('nav-up', 'Navigate to previous message', pathUp);

            const counterWrapper = document.createElement('div');
            counterWrapper.className = 'counter-wrapper';

            const counter = document.createElement('span');
            counter.id = 'chat-nav-counter';
            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', 'Navigate to next message', pathDown);
            const btnBottom = this.createButton('nav-bottom', 'Go to last message (End)', pathBottom);
            const menuContainer = document.createElement('div');
            menuContainer.id = 'chat-nav-menu-container';
            menuContainer.tabIndex = -1;
            menuContainer.setAttribute('role', 'menu');
            counterWrapper.append(counter, menuContainer);
            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();
            }
        },

        createButton(id, ariaLabel, pathData) {
            const button = document.createElement('button');
            button.id = id;
            button.className = 'chat-nav-button';
            button.setAttribute('aria-label', ariaLabel);
            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;
        },

        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);
        },

        attachNavigationLogic(btnTop, btnUp, btnDown, btnBottom, counter, menuContainer) {
            const lastIndex = () => this.allTurns.length - 1;

            this.setupHoldableButton(btnTop, () => this.scrollToAbsoluteTop());

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

                if (this.currentIndex === last && last > -1 && scrollContainer &&
                    Math.abs(scrollContainer.scrollTop - (scrollContainer.scrollHeight - scrollContainer.clientHeight)) < 5) {

                    this.isUnstickingFromBottom = true;
                    this.isScrollingProgrammatically = true;
                    scrollContainer.scrollTo({ top: scrollContainer.scrollHeight - 50, behavior: 'auto' });

                    await new Promise(resolve => setTimeout(async () => {
                        this.isUnstickingFromBottom = false;
                        if (this.currentIndex > 0) {
                            await this.navigateToIndex(this.currentIndex - 1, 'center');
                        } else {
                            this.isScrollingProgrammatically = false;
                        }
                        resolve();
                    }, 150));
                    return;
                }
                await this.navigateToIndex(this.currentIndex - 1, 'center');
            });

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

            this.setupHoldableButton(btnBottom, async () => {
                this.synchronizeCurrentIndexFromView();
                const last = lastIndex();
                if (this.currentIndex === last) {
                    const scrollContainer = document.querySelector('ms-autoscroll-container');
                    if (scrollContainer) {
                        this.isScrollingProgrammatically = true;
                        scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' });
                        setTimeout(() => { this.isScrollingProgrammatically = false; }, 800);
                    }
                } else {
                    await this.navigateToIndex(last, 'center');
                }
            });

            counter.addEventListener('click', (e) => { e.stopPropagation(); this.toggleNavMenu(); });
            counter.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); this.toggleNavMenu(); } });
            menuContainer.addEventListener('keydown', (e) => {
                const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
                if (!items.length) return;
                let newIndex = this.menuFocusedIndex;
                let shouldUpdateFocus = true;
                switch (e.key) {
                    case 'ArrowDown': e.preventDefault(); newIndex = (this.menuFocusedIndex + 1) % items.length; break;
                    case 'ArrowUp': e.preventDefault(); newIndex = (this.menuFocusedIndex - 1 + items.length) % items.length; break;
                    case 'PageDown': e.preventDefault(); newIndex = Math.min(items.length - 1, this.menuFocusedIndex + this.JUMP_DISTANCE); break;
                    case 'PageUp': e.preventDefault(); newIndex = Math.max(0, this.menuFocusedIndex - this.JUMP_DISTANCE); break;
                    case 'Home': e.preventDefault(); newIndex = 0; break;
                    case 'End': e.preventDefault(); newIndex = items.length - 1; break;
                    case 'Enter': e.preventDefault(); if (this.menuFocusedIndex !== -1) { items[this.menuFocusedIndex].click(); } shouldUpdateFocus = false; break;
                    case 'Escape': e.preventDefault(); this.toggleNavMenu(); shouldUpdateFocus = false; break;
                    default: shouldUpdateFocus = false; break;
                }
                if (shouldUpdateFocus && newIndex !== this.menuFocusedIndex) { this.updateMenuFocus(items, newIndex); }
            });
        },

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

            this.isScrollingProgrammatically = true;

            scrollContainer.scrollTo({
                top: 0,
                behavior: this.holdInterval ? 'auto' : 'smooth'
            });

            if (this.allTurns.length > 0 && this.currentIndex !== 0) {
                this.updateHighlight(this.currentIndex, 0);
                this.currentIndex = 0;
                this.updateCounterDisplay();
            }

            setTimeout(() => {
                this.isScrollingProgrammatically = false;
            }, 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;
            }

            targetTurn.scrollIntoView({ behavior: 'auto', block: 'center' });

            try {
                await this.waitForTurnToStabilize(targetTurn, 2000);

                let targetScrollTop;
                if (blockPosition === 'start') {
                    targetScrollTop = targetTurn.offsetTop;
                } else if (blockPosition === 'end') {
                    targetScrollTop = targetTurn.offsetTop - (scrollContainer.clientHeight - targetTurn.offsetHeight);
                } else { // 'center' or default
                    targetScrollTop = targetTurn.offsetTop - (scrollContainer.clientHeight * 0.3);
                }

                targetScrollTop = Math.max(0, targetScrollTop);
                targetScrollTop = Math.min(targetScrollTop, scrollContainer.scrollHeight - scrollContainer.clientHeight);

                scrollContainer.scrollTo({ top: targetScrollTop, behavior: isSmooth ? 'smooth' : 'auto' });

                const timeoutDuration = isSmooth ? 800 : 50;
                await new Promise(resolve => setTimeout(resolve, timeoutDuration));

            } catch (error) {
                console.warn('QuickNav:', error.message, 'Proceeding with final position.');
            } finally {
                this.isScrollingProgrammatically = false;
            }
        },

        async navigateToIndex(newIndex, blockPosition = 'center') {
            if (newIndex < 0 || newIndex >= this.allTurns.length) return;

            const oldIndex = this.currentIndex;

            if (newIndex < this.allTurns.length - 1) this.isDownButtonAtEndToggle = false;

            this.currentIndex = newIndex;
            this.updateHighlight(oldIndex, newIndex);
            this.updateCounterDisplay();
            await this.scrollToTurn(newIndex, blockPosition);
            this.updateScrollPercentage();
        },

        updateScrollPercentage() {
            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater) return;
            if (this.currentIndex < 0) {
                floater.style.visibility = 'hidden';
                return;
            }
            const currentTurn = this.allTurns[this.currentIndex];
            if (!currentTurn) {
                floater.style.visibility = 'hidden';
                return;
            }

            const rect = currentTurn.getBoundingClientRect();
            const turnHeight = rect.height;
            const isVisible = rect.bottom > 8 && rect.top < window.innerHeight;

            if (!isVisible || turnHeight <= 0) {
                floater.style.visibility = 'hidden';
                return;
            }

            floater.style.visibility = 'visible';
            const floaterHeight = floater.offsetHeight || 40;
            const idealTop = (window.innerHeight / 2) - (floaterHeight / 2);

            const upperBound = Math.max(8, rect.top);
            const lowerBound = Math.min(window.innerHeight, rect.bottom) - floaterHeight - 4;

            let finalTop = idealTop;
            finalTop = Math.max(finalTop, upperBound);
            finalTop = Math.min(finalTop, lowerBound);

            floater.style.top = `${finalTop}px`;
            floater.style.left = `${rect.right - (floater.offsetWidth / 2)}px`;

            const percentageBadge = document.getElementById('quicknav-badge-percentage');
            if (!percentageBadge) return;

            const viewportCenterY = window.innerHeight / 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)}%`;
        },

        synchronizeCurrentIndexFromView() {
            if (this.isUnstickingFromBottom || this.isScrollingProgrammatically || this.allTurns.length === 0) {
                if (this.allTurns.length === 0) {
                    this.updateHighlight(this.currentIndex, -1);
                    this.currentIndex = -1;
                }
                this.updateCounterDisplay();
                return;
            }

            if (this.currentIndex > -1 && this.currentIndex < this.allTurns.length) {
                const currentTurn = this.allTurns[this.currentIndex];
                const rect = currentTurn.getBoundingClientRect();
                if (rect.top < window.innerHeight && rect.bottom > 0) {
                    this.updateScrollPercentage();
                    return;
                }
            }

            const focusPointY = window.innerHeight * 0.35;
            let bestMatch = { index: -1, delta: Infinity };
            this.allTurns.forEach((turn, index) => {
                const rect = turn.getBoundingClientRect();
                if (rect.top < window.innerHeight && rect.bottom > 0) {
                    const delta = Math.abs(rect.top - focusPointY);
                    if (delta < bestMatch.delta) {
                        bestMatch = { index, delta };
                    }
                }
            });

            if (bestMatch.index !== -1 && this.currentIndex !== bestMatch.index) {
                this.updateHighlight(this.currentIndex, bestMatch.index);
                this.currentIndex = bestMatch.index;
                this.updateCounterDisplay();
                this.updateScrollPercentage();
            }
        },

        updateHighlight(oldIndex, newIndex) {
            if (oldIndex > -1 && oldIndex < this.allTurns.length) {
                this.allTurns[oldIndex].classList.remove('prompt-turn-highlight', 'response-turn-highlight');
            }

            const floater = document.getElementById('quicknav-badge-floater');
            if (!floater) return;

            if (newIndex > -1 && newIndex < this.allTurns.length) {
                const newTurn = this.allTurns[newIndex];
                const isPrompt = this.getTurnType(newTurn) === 'user_prompt';
                newTurn.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;
                floater.style.visibility = 'visible';
                this.updateScrollPercentage();
            } else {
                floater.style.visibility = 'hidden';
            }
        },

        updateCounterDisplay() {
            let currentNumSpan = document.getElementById('chat-nav-current-num');
            let totalNumSpan = document.getElementById('chat-nav-total-num');
            const counterContainer = document.getElementById('chat-nav-counter');

            if (!currentNumSpan || !totalNumSpan || !counterContainer) {
                const counter = document.getElementById('chat-nav-counter');
                if (counter) {
                    const current = this.currentIndex > -1 ? this.currentIndex + 1 : '-';
                    counter.textContent = `${current} / ${this.allTurns.length}`;
                }
                return;
            }

            const current = this.currentIndex > -1 ? this.currentIndex + 1 : '-';
            const total = this.allTurns.length;

            currentNumSpan.textContent = current;
            totalNumSpan.textContent = total;

            currentNumSpan.classList.remove('chat-nav-current-grey', 'chat-nav-current-blue');
            if (this.currentIndex === total - 1 && total > 0) {
                currentNumSpan.classList.add('chat-nav-current-blue');
            } else {
                currentNumSpan.classList.add('chat-nav-current-grey');
            }
        },

        toggleNavMenu() {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            const counter = document.getElementById('chat-nav-counter');
            if (!menuContainer || !counter) return;
            const isVisible = menuContainer.style.display === 'flex';
            if (isVisible) {
                menuContainer.style.display = 'none';
                counter.setAttribute('aria-expanded', 'false');
                document.removeEventListener('click', this.closeNavMenu, true);
                this.stopDynamicMenuLoading();
                counter.focus();
            } else {
                this.populateNavMenu();
                menuContainer.style.display = 'flex';
                counter.setAttribute('aria-expanded', 'true');
                menuContainer.focus();
                const items = menuContainer.querySelectorAll('.chat-nav-menu-item');
                const initialFocusIndex = this.currentIndex > -1 ? this.currentIndex : 0;
                this.updateMenuFocus(items, initialFocusIndex, false);
                requestAnimationFrame(() => {
                    const counterRect = counter.getBoundingClientRect();
                    const availableSpace = counterRect.top - 18;
                    menuContainer.style.maxHeight = `${availableSpace}px`;
                    if (this.menuFocusedIndex > -1 && this.menuFocusedIndex < items.length) {
                        const menuList = document.getElementById('chat-nav-menu');
                        const focusedItem = items[this.menuFocusedIndex];
                        if (menuList && focusedItem) {
                            menuList.scrollTop = focusedItem.offsetTop - menuList.offsetTop - (menuList.clientHeight / 2) + (focusedItem.clientHeight / 2);
                        }
                    }
                });
                setTimeout(() => document.addEventListener('click', this.closeNavMenu, true), 0);
            }
        },

        closeNavMenu(e) {
            const menuContainer = document.getElementById('chat-nav-menu-container');
            if (menuContainer && !menuContainer.parentElement.contains(e.target) && menuContainer.style.display === 'flex') {
                this.toggleNavMenu();
            }
        },

        updateMenuFocus(items, newIndex, shouldScroll = true) {
            if (!items || items.length === 0 || newIndex < 0 || newIndex >= items.length) return;
            if (this.menuFocusedIndex > -1 && this.menuFocusedIndex < items.length) {
                items[this.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;
                    }
                }
            }
            this.menuFocusedIndex = newIndex;
        },

        getTextFromTurn(turn, fromDOMOnly = false) {
            turn.isFallbackContent = false;
            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' };
                    }
                }
            }
            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 = ` ${codeContent.textContent} `;
                        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' };
            }
            turn.isFallbackContent = true;
            return { display: '...', full: 'Could not extract content.', source: 'fallback' };
        },

        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';
            this.allTurns.forEach((turn, index) => {
                let displayContent;
                if (turn.cachedContent && !turn.isFallbackContent) {
                    displayContent = turn.cachedContent;
                } else {
                    displayContent = this.getTextFromTurn(turn);
                }
                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 = this.getTurnType(turn) === 'user_prompt';
                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.title = full.replace(/\s+/g, ' ');
                item.addEventListener('click', () => {
                    this.toggleNavMenu();
                    this.navigateToIndex(index);
                });
                menuList.appendChild(item);
            });
            const leftContainer = document.createElement('div');
            leftContainer.style.display = 'flex';
            leftContainer.style.alignItems = 'center';
            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();
                this.startDynamicMenuLoading();
            });
            const statusIndicator = document.createElement('span');
            statusIndicator.id = 'chat-nav-loader-status';
            leftContainer.append(loadButton, statusIndicator);
            const donateButton = document.createElement('a');
            donateButton.href = 'https://nowpayments.io/embeds/donation-widget?api_key=0fe4e67c-64aa-4a74-b2d2-a91608b1ccc6';
            donateButton.target = '_blank';
            donateButton.rel = 'noopener noreferrer';
            donateButton.className = 'header-button';
            donateButton.textContent = 'Donate ☕';
            donateButton.title = 'Support the developer';
            donateButton.addEventListener('click', e => e.stopPropagation());
            header.appendChild(leftContainer);
            header.appendChild(donateButton);
            menuContainer.appendChild(header);
            menuContainer.appendChild(menuList);
        },

        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;

            this.originalCurrentIndex = this.currentIndex;
            this.originalScrollTop = scrollContainer.scrollTop;

            const menuItems = document.querySelectorAll('#chat-nav-menu .chat-nav-menu-item');
            this.loadingQueue = this.allTurns
                .map((turn, index) => ({ turn, index, menuItem: menuItems[index] }))
                .filter(item => {
                    const text = item.menuItem.title;
                    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;
                this.isQueueProcessing = true;
                await this.forceScrollToTop(scrollContainer);
                this.processLoadingQueue();
            } else {
                const statusIndicator = document.getElementById('chat-nav-loader-status');
                if (statusIndicator) statusIndicator.textContent = 'All loaded.';
            }
        },

        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 = this.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}...`;
                const itemToLoad = this.loadingQueue.shift();
                const { turn, index, menuItem } = itemToLoad;
                const textSpan = menuItem.querySelector('.menu-item-text');
                if (!turn || !textSpan) continue;
                this.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.title = newContent.full.replace(/\s+/g, ' ');
                } catch (error) {
                    console.error(`Failed to load item ${index + 1}:`, error.message);
                    textSpan.textContent = '[Error]';
                }
            }

            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 items = menuContainer ? menuContainer.querySelectorAll('.chat-nav-menu-item') : [];
            if (menuContainer && menuContainer.style.display === 'flex' && this.currentIndex > -1 && items.length > 0) {
                this.updateMenuFocus(items, this.currentIndex, true);
            }

            const loadButton = document.getElementById('chat-nav-load-button');
            if (loadButton) loadButton.disabled = false;
            if (statusIndicator) statusIndicator.textContent = this.loadingQueue.length > 0 ? 'Stopped.' : 'Done.';
        },

        stopDynamicMenuLoading() {
            if (!this.isQueueProcessing) return;
            this.isQueueProcessing = false;
            const loadButton = document.getElementById('chat-nav-load-button');
            if (loadButton) loadButton.disabled = false;
            const statusIndicator = document.getElementById('chat-nav-loader-status');
            if (statusIndicator) statusIndicator.textContent = 'Stopped.';
        }
    };

    ChatNavigator.closeNavMenu = ChatNavigator.closeNavMenu.bind(ChatNavigator);
    ChatNavigator.init();
})();