Google AI Studio | Conversation/Chat MarkDown-Export/Download

Export AI Studio conversations to Markdown with intelligent mode detection, toolbar integration, and abortable processing. Features dual-mode extraction and configurable filters.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Google AI Studio | Conversation/Chat MarkDown-Export/Download
// @namespace    http://violentmonkey.net/
// @version      2.5
// @description  Export AI Studio conversations to Markdown with intelligent mode detection, toolbar integration, and abortable processing. Features dual-mode extraction and configurable filters.
// @author       Vibe-Coded by Piknockyou
// @license      MIT
// @match        https://aistudio.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=aistudio.google.com
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

/**
 * AI Studio Export Script - Advanced Conversation Capture Tool
 * ============================================================
 *
 * A comprehensive userscript that exports Google AI Studio conversations to Markdown format
 * with intelligent mode detection, automated Raw Mode toggle, and high-performance scrolling.
 *
 * CREDITS & INSPIRATION
 * ---------------------
 * This script builds upon concepts from two projects:
 *
 * 1. Userscript: Google AI Studio 聊天记录导出器** (now deleted)
 *    • author: qwerty
 *    • Uploader: https://greasyfork.org/en/users/1462896-max-tab
 *    • Script: https://greasyfork.org/en/scripts/534177-google-ai-studio-chat-record-exporter
 *
 * 2. Extension: Gemini to PDF
 *    • Developer: Hamza Wasim ([email protected])
 *    • Extension: https://chromewebstore.google.com/detail/gemini-to-pdf/blndbnmpkgfoopgmcejnhdnepfejgipe
 *    • Website: https://geminitoolbox.com/
 *    • Announcement: https://www.reddit.com/r/Bard/comments/1lshanb/built_a_free_extension_to_save_gemini_and_ai/
 *
 * CORE FEATURES
 * -------------
 * • Intelligent Mode Detection: Auto-detects Raw vs Rendered Mode
 * • Abortable Processing: Cancel extraction mid-process
 * • Toolbar Integration: Seamlessly integrated into AI Studio's toolbar
 * • Configurable Filters: Real-time control over export content
 * • High-Performance: 50ms scroll delays with smart batching
 * • Auto-Recovery: Handles dynamic UI re-rendering automatically
 * • CSP-Compliant: Full compliance with AI Studio's security policies
 *
 * USER INTERFACE
 * --------------
 * • Export Button: Toolbar icon with visual state feedback (Idle/Working/Success/Error)
 * • Settings Panel: Gear icon opens configuration with export filters
 * • Abort Control: Click button during processing to cancel operation
 * • Dynamic Positioning: Panel adapts to window changes
 *
 * CONFIGURATION
 * -------------
 * • Prefer Raw Mode: Auto-switch to Raw Mode for cleaner extraction
 * • Include User Messages: Control user prompt inclusion
 * • Include AI Responses: Control AI answer inclusion
 * • Include AI Thinking: Control AI reasoning/thought process inclusion
 *
 * TECHNICAL ARCHITECTURE
 * ----------------------
 * • MutationObserver: Watches toolbar changes for auto-recovery
 * • AbortController: Implements responsive cancellation
 * • Multi-Strategy DOM: Fallback selectors for reliable extraction
 * • Memory Efficient: DOM element keys for data association
 * • Scroll Intelligence: Advanced detection of conversation boundaries
 *
 * USAGE
 * -----
 * 1. Find export/download icon in AI Studio toolbar
 * 2. Click gear icon to configure export filters (optional)
 * 3. Click export button to begin automated capture
 * 4. Monitor progress via button state changes
 * 5. Click button again to abort if needed
 * 6. Download starts automatically when complete
 *
 * PERFORMANCE
 * -----------
 * • Ultra-Fast: 50ms scroll delays with intelligent batching
 * • Smart Detection: Multiple passes ensure complete capture
 * • Error Recovery: Automatic retry with graceful failure handling
 * • Browser Support: Compatible with Tampermonkey, Violentmonkey
 */

(function() {
    'use strict';

    //================================================================================
    // CONFIGURATION - All script settings, constants, and tunable parameters
    //================================================================================

    //--------------------------------------------------------------------------------
    // CORE EXPORT PREFERENCES - The most important settings for the user
    //--------------------------------------------------------------------------------
    // Mode Preference: Set to true to extract in "Raw Mode" (clean markdown), or false for "Rendered Mode".
    let PREFER_RAW_MODE = true;

    // Content Filtering: Control which parts of the conversation to include in the export.
    let INCLUDE_USER_MESSAGES = true;    // true to include user prompts/questions
    let INCLUDE_AI_RESPONSES = true;     // true to include the AI's main answers/replies
    let INCLUDE_AI_THINKING = false;     // true to include the AI's reasoning/thought process

    //--------------------------------------------------------------------------------
    // FILE OUTPUT SETTINGS - Customize the exported file name
    //--------------------------------------------------------------------------------
    const EXPORT_FILENAME_PREFIX = 'aistudio_chat_export_';

    //--------------------------------------------------------------------------------
    // CORE EXTRACTION BEHAVIOR - Settings that affect how content is captured
    //--------------------------------------------------------------------------------
    const THOUGHT_EXPAND_DELAY_MS = 500;       // Wait time after expanding "thinking" sections to allow content to load.
    const THOUGHT_MIN_LENGTH = 10;             // Minimum text length for a "thinking" block to be considered valid.

    //--------------------------------------------------------------------------------
    // PERFORMANCE & TIMING - Advanced settings to balance speed and reliability
    //--------------------------------------------------------------------------------
    const SCROLL_DELAY_MS = 50;                // Main scroll delay: 50ms for "machine-gun" speed. Lower values are faster but may miss content on slow connections.
    const RAW_MODE_MENU_DELAY_MS = 200;        // Wait for the "Raw Mode" dropdown menu to appear after clicking "More".
    const RAW_MODE_RENDER_DELAY_MS = 300;      // Wait for the UI to re-render after toggling Raw Mode.
    const FINAL_CAPTURE_DELAY_MS = 50;         // Delay before the final data extraction pass after scrolling is complete.
    const POST_EXTRACTION_DELAY_MS = 100;      // Delay for any cleanup or final processing after extraction.
    const FINAL_COLLECTION_DELAY_MS = 300;     // Delay at each final scroll position (top, middle, bottom) to ensure all content is loaded.
    const SUCCESS_RESET_TIMEOUT_MS = 2500;     // Time in ms before the button resets from 'Success' to 'Idle' (2.5 seconds).
    const ERROR_RESET_TIMEOUT_MS = 4000;       // Time in ms before the button resets from 'Failure' to 'Idle' (4 seconds).
    const UPWARD_SCROLL_DELAY_MS = 1000;       // Delay for content to load when starting a scroll-up operation from the bottom.

    //--------------------------------------------------------------------------------
    // SCROLL BEHAVIOR - Advanced parameters controlling the auto-scrolling mechanism
    //--------------------------------------------------------------------------------
    const MAX_SCROLL_ATTEMPTS = 10000;         // Maximum scroll attempts before giving up to prevent infinite loops.
    const SCROLL_INCREMENT_INITIAL = 150;      // Initial scroll increment in pixels per step.
    const SCROLL_INCREMENT_LARGE = 500;        // A larger initial jump for the first scroll pass to load content faster.
    const BOTTOM_DETECTION_TOLERANCE = 10;     // Pixel tolerance for detecting the bottom of the scroll area.
    const PROBLEMATIC_JUMP_FACTOR = 1.25;      // Factor to detect problematic scroll jumps (e.g., 125% of intended distance).
    const MIN_SCROLL_DISTANCE_THRESHOLD = 5;   // Minimum pixel distance to detect if scrolling has effectively stopped.
    const SCROLL_PARENT_SEARCH_DEPTH = 5;      // How many parent elements to search upwards to find the main scroll container.


    //--------------------------------------------------------------------------------
    // LOG CONSOLE UI - Styling for the debug log panel
    //--------------------------------------------------------------------------------
    const LOG_ENTRY_INFO_COLOR = '#e8eaed';
    const LOG_ENTRY_SUCCESS_COLOR = '#34a853';
    const LOG_ENTRY_WARN_COLOR = '#fbbc04';
    const LOG_ENTRY_ERROR_COLOR = '#ea4335';

    // State variables
    let isScrolling = false;
    let collectedData = new Map();
    let scrollCount = 0;
    let noChangeCounter = 0;
    let scrollIncrement = SCROLL_INCREMENT_LARGE;
    let exportButtonState = 'IDLE'; // Can be 'IDLE', 'WORKING', 'SUCCESS', 'ERROR'
    let abortController;

     // UI Elements
    let exportButton, exportIcon;
    let settingsPanel;
    let preferRawModeCheckbox, includeUserMessagesCheckbox, includeAiResponsesCheckbox, includeAiThinkingCheckbox;

    //================================================================================
    // HELPER FUNCTIONS - Utility functions for script operation
    //================================================================================

    /**
     * Creates a Trusted Types policy to handle Content Security Policy compliance
     * for DOM manipulation in Google AI Studio's environment.
     */
    let trustedTypesPolicy = null;
    if (window.trustedTypes && window.trustedTypes.createPolicy) {
        try {
            // Policy names for userscripts often require a specific format.
            trustedTypesPolicy = window.trustedTypes.createPolicy('aistudio-export-policy#userscript', {
                createHTML: string => string
            });
        } catch (e) {
            console.warn("[AI Studio Export] Could not create a new Trusted Types policy. This might happen if the policy already exists. The script will attempt to continue.", e.message);
        }
    }

    /**
     * Creates a trusted HTML string for DOM manipulation while respecting CSP policies.
     * @param {string} htmlString - The HTML string to be processed.
     * @returns {string} - A trusted HTML string compatible with Content Security Policy.
     */
    function getTrustedHTML(htmlString) {
        if (trustedTypesPolicy) {
            return trustedTypesPolicy.createHTML(htmlString);
        }
        return htmlString;
    }

    /**
     * Creates a delay/promise for asynchronous operations.
     * @param {number} ms - Time in milliseconds to delay.
     * @returns {Promise} - Promise that resolves after the specified delay.
     */
    function delay(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    /**
     * Generates a timestamp string in format YYYYMMDD_HHMMSS for file naming.
     * @returns {string} - Formatted timestamp string.
     */
    function getCurrentTimestamp() {
        const n = new Date();
        const YYYY = n.getFullYear();
        const MM = (n.getMonth() + 1).toString().padStart(2, '0');
        const DD = n.getDate().toString().padStart(2, '0');
        const hh = n.getHours().toString().padStart(2, '0');
        const mm = n.getMinutes().toString().padStart(2, '0');
        const ss = n.getSeconds().toString().padStart(2, '0');
        return `${YYYY}${MM}${DD}_${hh}${mm}${ss}`;
    }

    /**
     * Logs a message to the browser console with consistent formatting.
     * @param {string} message - The message to log.
     */
    function logToConsole(message) {
        console.log(`[AI Studio Export] ${message}`);
    }

    /**
     * Logs a message to both console and visual panel for consistent output.
     * This function replaces the old logToConsole calls throughout the script.
     * @param {string} message - The message to log.
     * @param {string} type - The log type: 'info', 'success', 'warn', 'error' (optional).
     */
    function log(message, type = 'info') {
        const style = `color: ${type === 'success' ? LOG_ENTRY_SUCCESS_COLOR : type === 'warn' ? LOG_ENTRY_WARN_COLOR : type === 'error' ? LOG_ENTRY_ERROR_COLOR : LOG_ENTRY_INFO_COLOR};`;
        console.log(`%c[AI Studio Export] ${message}`, style);
    }

    /**
     * Detects whether the page is currently in Raw Mode or Rendered Mode.
     * Raw Mode shows plain markdown text in .very-large-text-container
     * Rendered Mode shows formatted content in ms-cmark-node elements
     * @returns {string} - 'raw' or 'rendered'
     */
    function detectCurrentMode() {
        // Strategy 1: Check for Raw Mode indicator in a user turn
        const firstUserTurn = document.querySelector('ms-chat-turn .chat-turn-container.user');
        if (firstUserTurn) {
            const hasRawContainer = firstUserTurn.querySelector('ms-text-chunk .very-large-text-container');
            const hasCmarkNode = firstUserTurn.querySelector('ms-text-chunk ms-cmark-node');

            if (hasRawContainer && !hasCmarkNode) {
                log("Detected mode: Raw Mode", 'info');
                return 'raw';
            }
            if (hasCmarkNode && !hasRawContainer) {
                log("Detected mode: Rendered Mode", 'info');
                return 'rendered';
            }
        }

        // Strategy 2: Check URL or page state
        // (AI Studio might have a parameter or class indicating mode)
        const body = document.body;
        if (body.classList.contains('raw-mode')) {
            log("Detected mode: Raw Mode (via body class)", 'info');
            return 'raw';
        }

        // Default assumption (most common state)
        log("Could not detect mode, assuming Rendered Mode", 'warn');
        return 'rendered';
    }

    /**
     * Expands collapsed AI thinking sections to expose hidden content.
     * Uses multiple selector strategies to find and expand thinking panels.
     * @async
     * @param {Element} modelDiv - The model DOM element to expand thinking in.
     * @param {number} turnIndex - The index of the turn being processed.
     * @returns {Promise<boolean>} - True if thinking sections were expanded, false otherwise.
     */
    async function expandThinkingSections(modelDiv, turnIndex = 0) {
        let expanded = false;

        try {
            // Strategy 1: Find collapsed expansion panels with thought-related text
            const collapsedPanels = modelDiv.querySelectorAll('mat-expansion-panel[aria-expanded="false"]');
            for (const panel of collapsedPanels) {
                const headerText = panel.querySelector('.mat-expansion-panel-header-title')?.textContent?.toLowerCase() || '';
                const buttonText = panel.querySelector('button[aria-expanded="false"]')?.textContent?.toLowerCase() || '';

                if (headerText.includes('thought') || headerText.includes('thinking') ||
                    buttonText.includes('thought') || buttonText.includes('thinking')) {
                    const expandButton = panel.querySelector('button[aria-expanded="false"]');
                    if (expandButton) {
                        expandButton.click();
                        expanded = true;
                        log(`Expanded thinking section (panel method) for turn ${turnIndex}`, 'info');
                    }
                }
            }

            // Strategy 2: Find expand buttons with thought-related text
            const expandButtons = modelDiv.querySelectorAll('button');
            for (const button of expandButtons) {
                const buttonText = button.textContent?.toLowerCase() || '';
                if ((buttonText.includes('expand') || buttonText.includes('show more')) &&
                    buttonText.includes('thought')) {
                    button.click();
                    expanded = true;
                    log(`Expanded thinking section (button method) for turn ${turnIndex}`, 'info');
                }
            }

            // Strategy 3: Click any "Show more" buttons in thought chunks
            const thoughtChunks = modelDiv.querySelectorAll('ms-thought-chunk');
            for (const chunk of thoughtChunks) {
                const showMoreButton = chunk.querySelector('button[aria-expanded="false"], button:not([aria-expanded])');
                if (showMoreButton && showMoreButton.textContent?.toLowerCase().includes('more')) {
                    showMoreButton.click();
                    expanded = true;
                    log(`Expanded thinking section (chunk method) for turn ${turnIndex}`, 'info');
                }
            }

            // Wait for expansion to complete
            if (expanded) {
                await delay(THOUGHT_EXPAND_DELAY_MS);
            }

            return expanded;
        } catch (error) {
            log(`Error expanding thinking sections for turn ${turnIndex}: ${error.message}`, 'warn');
            return false;
        }
    }

    /**
     * Clean and validate HTML snippet, removing unwanted elements.
     * Uses CSP-compliant DOM parsing to handle AI Studio's strict Content Security Policy.
     * @param {string} htmlSnippet - The HTML content to be cleaned and validated.
     * @returns {string|null} - The cleaned HTML string, or null if validation fails.
     */
    function cleanAndValidateSnippet(htmlSnippet) {
        if (!htmlSnippet || "string" !== typeof htmlSnippet) {
            return null;
        }

        let docFragment;
        try {
            // Use Range.createContextualFragment to safely parse HTML, avoiding DOMParser CSP issues.
            const range = document.createRange();
            range.selectNode(document.body); // Set a context for parsing.
            docFragment = range.createContextualFragment(htmlSnippet);
        } catch (parseError) {
            console.error("[AI Studio Export] Error creating contextual fragment during cleaning:", parseError, htmlSnippet.substring(0, 100));
            return null; // Return null if parsing fails
        }

        const turnDiv = docFragment.firstElementChild;
        if (!turnDiv) {
            console.warn("[AI Studio Export] Cleaning: Snippet did not contain a valid root element.", htmlSnippet.substring(0, 100));
            return null;
        }

        // Skip HTML cleaning due to strict CSP. Return raw HTML for fallback.
        // This method is CSP-compliant and won't trigger TrustedHTML errors.
        return htmlSnippet;
    }

    /**
     * Convert HTML to Markdown format with comprehensive formatting support.
     * Handles code blocks, links, images, inline code, emphasis, lists, headings, and paragraphs.
     * @param {string} html - The HTML content to convert to Markdown.
     * @returns {string} - The converted Markdown string.
     */
    function convertToMarkdown(html) {
        if (!html) return "";
        try {
            const tempDiv = document.createElement("div");
            tempDiv.innerHTML = getTrustedHTML(html);

            const ignoreStartRegex = /^\s*<!--\s*IGNORE_WHEN_COPYING_START\s*-->\s*(\r?\n)?/im;
            const ignoreEndRegex = /^\s*<!--\s*IGNORE_WHEN_COPYING_END\s*-->\s*(\r?\n)?/im;

            const sourceElement = tempDiv.querySelector(".turn-content") || tempDiv;

            // Process code blocks
            sourceElement.querySelectorAll("ms-code-block").forEach(block => {
                const pre = block.querySelector("pre");
                if (pre) {
                    pre.innerHTML = getTrustedHTML(pre.innerHTML.replaceAll(ignoreStartRegex, "").replaceAll(ignoreEndRegex, ""));
                }

                const code = pre ? pre.textContent : block.textContent.replaceAll(ignoreStartRegex, "").replaceAll(ignoreEndRegex, "");
                const langMatch = code.match(/^(\w+)\s*\n/);
                const lang = langMatch ? langMatch[1] : "";
                const codeContent = langMatch ? code.substring(langMatch[0].length) : code;

                block.replaceWith(`\n\n\`\`\`${lang}\n${codeContent.trim()}\n\`\`\`\n\n`);
            });

            // Process links
            sourceElement.querySelectorAll("a").forEach(a => {
                a.replaceWith(`[${a.textContent || a.innerText || ""}](${a.getAttribute("href") || ""})`);
            });

            // Process images
            sourceElement.querySelectorAll("img").forEach(img => {
                img.replaceWith(`\n\n![${img.getAttribute("alt") || "image"}](${img.getAttribute("src") || ""})\n\n`);
            });

            // Process inline code
            sourceElement.querySelectorAll("code:not(pre code)").forEach(code => {
                code.replaceWith(`\`${code.textContent}\``);
            });

            // Process emphasis
            sourceElement.querySelectorAll("strong, b").forEach(el => {
                el.replaceWith(`**${el.textContent}**`);
            });
            sourceElement.querySelectorAll("em, i").forEach(el => {
                el.replaceWith(`*${el.textContent}*`);
            });

            // Process lists
            sourceElement.querySelectorAll("ul, ol").forEach(list => {
                let listMd = "\n";
                Array.from(list.children).forEach((li, index) => {
                    let liContent = li.innerHTML.replace(/<br\s*\/?>/gi, "\n");
                    const tempLiDiv = document.createElement("div");
                    tempLiDiv.innerHTML = getTrustedHTML(liContent);

                    // Clean formatting within list items
                    tempLiDiv.querySelectorAll("strong, b").forEach(el => {
                        el.replaceWith(`**${el.textContent}**`);
                    });
                    tempLiDiv.querySelectorAll("em, i").forEach(el => {
                        el.replaceWith(`*${el.textContent}*`);
                    });
                    tempLiDiv.querySelectorAll("code:not(pre code)").forEach(el => {
                        el.replaceWith(`\`${el.textContent}\``);
                    });
                    tempLiDiv.querySelectorAll("a").forEach(el => {
                        el.replaceWith(`[${el.textContent}](${el.getAttribute("href")})`);
                    });

                    const content = tempLiDiv.textContent?.trim() || "";
                    listMd += `${list.tagName === "OL" ? index + 1 + "." : "*"} ${content}\n`;
                });
                list.replaceWith(listMd);
            });

            // Process headings
            sourceElement.querySelectorAll("h1,h2,h3,h4,h5,h6").forEach(h => {
                const level = parseInt(h.tagName.substring(1), 10);
                h.replaceWith(`\n\n${"#".repeat(level)} ${h.textContent}\n\n`);
            });

            // Process paragraphs
            sourceElement.querySelectorAll("p").forEach(p => {
                const next = p.nextElementSibling;
                if (!p.textContent.trim() || (next && ["H1", "H2", "H3", "H4", "H5", "H6", "UL", "OL", "PRE", "BLOCKQUOTE", "HR", "TABLE", "MS-CODE-BLOCK"].includes(next?.tagName))) {
                    return; // Skip empty paragraphs and those followed by block elements
                }
                p.after("\n\n");
            });

            let text = sourceElement.innerText || sourceElement.textContent || "";
            return text = text.replace(/(\n\s*){3,}/g, "\n\n"), text.trim();

        } catch (e) {
            console.error("Error converting HTML snippet to Markdown:", e, html.substring(0, 100));
            const tempDiv = document.createElement("div");
            tempDiv.innerHTML = getTrustedHTML(html);
            return tempDiv.textContent || "";
        }
    }

    //================================================================================
    // CORE FUNCTIONS - Main business logic and automation
    //================================================================================

    /**
     * Automates the Raw Mode toggle functionality in Google AI Studio.
     * This function finds and clicks the "more actions" button, then selects "Raw Mode"
     * from the dropdown menu to expose the pure Markdown content.
     * @async
     * @returns {Promise<boolean>} - True if Raw Mode was successfully toggled, false otherwise.
     */
    async function toggleRawMode() {
    log("Attempting to toggle Raw Mode...");

    // 1. Find and click the 'More' button
    const moreButton = document.querySelector('button[aria-label="View more actions"]');
    if (!moreButton) {
        log("Error: 'More actions' button not found.", 'error');
        return false;
    }
    moreButton.click();
    log("'More actions' button clicked.");

    // 2. Wait for the menu to appear and click the 'Raw Mode' button
    await delay(RAW_MODE_MENU_DELAY_MS); // Wait for menu animation

    const rawModeButton = Array.from(document.querySelectorAll('button[role="menuitem"]'))
                               .find(btn => btn.textContent.includes('Raw Mode'));

    if (!rawModeButton) {
        log("Error: 'Raw Mode' button not found in the menu.", 'error');
        // Attempt to close the menu by clicking the 'More' button again
        moreButton.click();
        return false;
    }

    rawModeButton.click();
    log("'Raw Mode' button clicked.");
    await delay(RAW_MODE_RENDER_DELAY_MS); // Wait for the UI to re-render after toggling
    return true;
}

    /**
     * Identifies and returns the main scrollable element for AI Studio conversations.
     * Uses a cascade of selectors starting with the most specific, then falling back
     * to more general approaches, finally defaulting to document.documentElement.
     * @returns {Element} - The identified scrollable element.
     */
    function getMainScrollerElement_AiStudio() {
        log("Searching for page scroll container...");

        // Try the proven selectors from the extension
        let scroller = document.querySelector('ms-autoscroll-container');
        if (scroller) {
            log("Found scroll container (ms-autoscroll-container)", 'success');
            return scroller;
        }

        // Fallback to chat turns parent
        const chatTurnsContainer = document.querySelector('ms-chat-turn')?.parentElement;
        if (chatTurnsContainer) {
            let parent = chatTurnsContainer;
            for (let i = 0; i < SCROLL_PARENT_SEARCH_DEPTH && parent; i++) {
                if (parent.scrollHeight > parent.clientHeight + BOTTOM_DETECTION_TOLERANCE &&
                    (window.getComputedStyle(parent).overflowY === 'auto' ||
                     window.getComputedStyle(parent).overflowY === 'scroll')) {
                    log("Found scroll container (searching parent elements)", 'success');
                    return parent;
                }
                parent = parent.parentElement;
            }
        }

        log("Warning: Using document.documentElement as fallback", 'warn');
        return document.documentElement;
    }

    /**
     * Extracts data from all visible chat turns in the current AI Studio conversation.
     * Uses DOM traversal with CSP-compliant selectors to capture user prompts,
     * AI responses, and AI thinking output. Maintains a Map of collected data.
     * @returns {boolean} - True if new data was found or existing data was updated, false otherwise.
     */
    async function extractDataIncremental_AiStudio(extractionMode = 'raw') {
        let newlyFoundCount = 0;
        let dataUpdatedInExistingTurn = false;
        const currentTurns = document.querySelectorAll('ms-chat-turn');

        for (const [index, turn] of currentTurns.entries()) {
            const turnKey = turn; // Use turn element as key (consistent with MaxTab)
            const turnContainer = turn.querySelector('.chat-turn-container.user, .chat-turn-container.model');
            if (!turnContainer) continue; // Skip only if no container found

            // ✅ Check if new turn, but DON'T skip existing ones
            let isNewTurn = !collectedData.has(turnKey);
            let extractedInfo = collectedData.get(turnKey) || {
                domOrder: index,
                type: 'unknown',
                userText: null,
                thoughtText: null,
                responseText: null
            };

            if (isNewTurn) {
                collectedData.set(turnKey, extractedInfo);
                newlyFoundCount++;
            }

            let dataWasUpdatedThisTime = false;

            // Extract based on container type (exactly like MaxTab)
            if (turnContainer.classList.contains('user')) {
                if (extractedInfo.type === 'unknown') extractedInfo.type = 'user';
                if (!extractedInfo.userText) {
                    let userText = null;
                    if (extractionMode === 'raw') {
                        // RAW MODE: .very-large-text-container
                        const rawContainer = turn.querySelector('ms-text-chunk .very-large-text-container');
                        if (rawContainer) {
                            userText = rawContainer.textContent.trim();
                        }
                    } else {
                        // RENDERED MODE: ms-cmark-node
                        let userNode = turn.querySelector('.turn-content ms-cmark-node');
                        if (userNode) {
                            userText = userNode.innerText.trim();
                        }
                    }
                    if (userText) {
                        extractedInfo.userText = userText;
                        dataWasUpdatedThisTime = true;
                        log(`Extracted user text from turn ${index}: "${userText.substring(0, 50)}..."`);
                    }
                }
            } else if (turnContainer.classList.contains('model')) {
                if (extractedInfo.type === 'unknown') extractedInfo.type = 'model';

                // ✅ NEW: Expand collapsed thinking sections BEFORE extraction
                await expandThinkingSections(turn, index);

                // Extract AI thinking output with multiple strategies
                if (!extractedInfo.thoughtText) {
                    let thoughtText = null;

                    if (extractionMode === 'raw') {
                        // RAW MODE selector
                        const rawThought = turn.querySelector('ms-thought-chunk .very-large-text-container');
                        if (rawThought) {
                            thoughtText = rawThought.textContent.trim();
                        }
                    } else {
                        // RENDERED MODE selector
                        let thoughtNode = turn.querySelector('ms-thought-chunk .mat-expansion-panel-body ms-cmark-node');
                        if (thoughtNode) {
                            thoughtText = thoughtNode.textContent.trim();
                        }
                    }

                    if (thoughtText && thoughtText.length >= THOUGHT_MIN_LENGTH) {
                        extractedInfo.thoughtText = thoughtText;
                        dataWasUpdatedThisTime = true;
                        log(`Extracted AI thinking from turn ${index}: "${thoughtText.substring(0, 50)}..."`);
                    }
                }


                // Extract AI response (exactly like MaxTab)
                if (!extractedInfo.responseText) {
                    const responseChunks = Array.from(turn.querySelectorAll('.turn-content > ms-prompt-chunk'));
                    const responseTexts = responseChunks
                        .filter(chunk => !chunk.querySelector('ms-thought-chunk'))
                        .map(chunk => {
                            if (extractionMode === 'raw') {
                                // RAW MODE selector
                                const rawContainer = chunk.querySelector('ms-text-chunk .very-large-text-container');
                                if (rawContainer) {
                                    return rawContainer.textContent.trim();
                                }
                            } else {
                                // RENDERED MODE selector
                                const cmarkNode = chunk.querySelector('ms-cmark-node');
                                if (cmarkNode) {
                                    return cmarkNode.innerText.trim();
                                }
                            }
                            return chunk.innerText.trim(); // Fallback
                        })
                        .filter(text => text);

                    if (responseTexts.length > 0) {
                        extractedInfo.responseText = responseTexts.join('\n\n');
                        dataWasUpdatedThisTime = true;
                        log(`Extracted AI response from turn ${index} with ${responseTexts.length} chunks`);
                    } else if (!extractedInfo.thoughtText) {
                        // Fallback only if no thinking was found (like MaxTab)
                        const turnContent = turn.querySelector('.turn-content');
                        if (turnContent) {
                            extractedInfo.responseText = turnContent.innerText.trim();
                            dataWasUpdatedThisTime = true;
                            log(`Extracted AI response from turn ${index} using fallback`);
                        }
                    }
                }

                // Set turn type (exactly like MaxTab)
                if (dataWasUpdatedThisTime) {
                    if (extractedInfo.thoughtText && extractedInfo.responseText) extractedInfo.type = 'model_thought_reply';
                    else if (extractedInfo.responseText) extractedInfo.type = 'model_reply';
                    else if (extractedInfo.thoughtText) extractedInfo.type = 'model_thought';
                }
            }

            // Update collected data if anything changed
            if (dataWasUpdatedThisTime) {
                collectedData.set(turnKey, extractedInfo);
                dataUpdatedInExistingTurn = true;
            }
        }

        if (currentTurns.length > 0 && collectedData.size === 0) {
            log("Warning: Chat turns exist but no data could be extracted. Please check selectors.", 'warn');
        } else {
            log(`Scroll ${scrollCount}/${MAX_SCROLL_ATTEMPTS}... Found ${collectedData.size} total records`);
        }

        return newlyFoundCount > 0 || dataUpdatedInExistingTurn;
    }

    /**
     * Performs high-speed auto-scrolling through AI Studio conversations to capture all content.
     * Uses incremental scrolling with hardcoded delays for maximum speed, detecting end conditions
     * and handling problematic scroll jumps. Performs final collection passes for completeness.
     * @async
     * @param {string} direction - The direction to scroll ('down' or 'up').
     * @returns {Promise<boolean>} - True if the scrolling completed successfully, false otherwise.
     */
    /**
     * Preloads conversation history by repeatedly scrolling to the top.
     * This forces the AI Studio UI to load older, virtualized messages.
     * @async
     * @param {Element} scroller - The main scrollable element.
     */
    async function preloadHistory(scroller) {
        log("Preloading history by scrolling to top...");
        let lastHeight = 0;
        const isWindowScroller = (scroller === document.documentElement || scroller === document.body);
        const getScrollHeight = () => isWindowScroller ? document.documentElement.scrollHeight : scroller.scrollHeight;

        for (let i = 0; i < 5; i++) { // Loop a few times to ensure lazy-loaded content is fetched
            if (isWindowScroller) { window.scrollTo({ top: 0, behavior: 'instant' }); }
            else { scroller.scrollTo({ top: 0, behavior: 'instant' }); }

            await delay(UPWARD_SCROLL_DELAY_MS);

            const newHeight = getScrollHeight();
            if (newHeight <= lastHeight + MIN_SCROLL_DISTANCE_THRESHOLD) { // Use existing constant for stability check
                log(`History preloading stable at height: ${newHeight}px`, 'success');
                break;
            }
            lastHeight = newHeight;
            log(`Preloading... scrollHeight grew to ${newHeight}px`);
        }

        // After preloading, the scroller is at the top, ready for the main downward scroll.
        log("Preloading complete, starting capture from the top.");
    }

    async function autoScroll_AiStudio(direction = 'down') {
        log(`Starting auto-scroll (direction: ${direction})...`);
        isScrolling = true;
        collectedData.clear();
        scrollCount = 0;
        noChangeCounter = 0;
        scrollIncrement = SCROLL_INCREMENT_INITIAL; // Reset to initial value

        const scroller = getMainScrollerElement_AiStudio();
        if (!scroller) {
            log('Error: Cannot find scroll area!', 'error');
            alert('Unable to find chat scroll area. Auto-scroll cannot proceed.');
            isScrolling = false;
            return false;
        }

        log(`Using scroll element: ${scroller.tagName}.${scroller.className.split(' ').join('.')}`);

        const isWindowScroller = (scroller === document.documentElement || scroller === document.body);
        const getScrollTop = () => isWindowScroller ? window.scrollY : scroller.scrollTop;
        const getScrollHeight = () => isWindowScroller ? document.documentElement.scrollHeight : scroller.scrollHeight;
        const getClientHeight = () => isWindowScroller ? window.innerHeight : scroller.clientHeight;

        // Preload history to handle lazy-loading chats and position scroller at the top.
        await preloadHistory(scroller);

        log(`Starting incremental scroll (up to ${MAX_SCROLL_ATTEMPTS} attempts)...`);
        let lastScrollTop = -1;
        let reachedEnd = false;
        let scrollMessages = ["scrolling...", "scrolling..", "scrolling.", "scrolling"];

        // Initial collection
        const desiredMode = PREFER_RAW_MODE ? 'raw' : 'rendered';
        const initialNewCount = await extractDataIncremental_AiStudio(desiredMode);
        log(`Initial collection: ${collectedData.size} messages`);

        while (scrollCount < MAX_SCROLL_ATTEMPTS && !reachedEnd && isScrolling) {
            if (abortController.signal.aborted) {
                log('Scroll aborted by user.', 'warn');
                isScrolling = false;
                break;
            }
            const currentTop = scroller.scrollTop;
            const clientHeight = scroller.clientHeight;
            const scrollHeight = scroller.scrollHeight;

            // End detection: Check if we've reached the bottom.
            // IMPORTANT: `scrollCount > 0` prevents exiting on the first pass if already at the bottom.
            if (scrollCount > 0 && currentTop + clientHeight >= scrollHeight - BOTTOM_DETECTION_TOLERANCE) {
                log("Reached bottom of conversation - no more content to scroll", 'success');
                reachedEnd = true;
                break;
            }

            // Calculate the next scroll target position
            // Add the increment to the current position, but don't exceed the maximum possible scroll
            let intendedScrollTarget = currentTop + scrollIncrement;
            const maxPossibleScrollTop = scrollHeight - clientHeight;
            if (intendedScrollTarget > maxPossibleScrollTop) {
                intendedScrollTarget = maxPossibleScrollTop; // Clamp to bottom
            }

            // Execute the scroll operation
            scroller.scrollTop = intendedScrollTarget;
            scrollCount++;

            // Machine-gun speed delay: 50ms for maximum performance
            // May miss content on very slow connections
            await delay(SCROLL_DELAY_MS);

            const effectiveScrollTop = scroller.scrollTop;
            const actualScrolledDistance = effectiveScrollTop - currentTop;

            // Detect problematic scroll jumps that might indicate UI issues
            // A jump > configured factor of intended distance suggests the UI may have re-arranged
            let isProblematicJump = false;
            if (actualScrolledDistance > PROBLEMATIC_JUMP_FACTOR * scrollIncrement && intendedScrollTarget < maxPossibleScrollTop - BOTTOM_DETECTION_TOLERANCE) {
                isProblematicJump = true;
                log(`Scroll jump detected. From: ${currentTop}, Aimed: ${intendedScrollTarget}, Got: ${effectiveScrollTop}`, 'warn');
            }

            // Detect when scrolling has effectively stopped (conversation end)
            // If we moved less than the minimum threshold and this isn't the first scroll, assume we're at the end
            if (actualScrolledDistance < MIN_SCROLL_DISTANCE_THRESHOLD && scrollCount > 1) {
                log("Scroll effectively stopped, assuming end of conversation", 'success');
                reachedEnd = true;
                break;
            }

            // Extract any newly visible messages after this scroll position
            const newlyAddedCount = await extractDataIncremental_AiStudio(desiredMode);

            // Update progress indicator with cycling messages for better UX
            const loadingMessage = scrollMessages[(scrollCount - 1) % scrollMessages.length];
            const indicatorMessage = `${loadingMessage} (Found ${collectedData.size} messages)`;

            log(indicatorMessage);

            // Update tracking variables for next iteration
            lastScrollTop = effectiveScrollTop;
        }

        // Handle different completion scenarios with detailed logging
        if (!isScrolling && scrollCount < MAX_SCROLL_ATTEMPTS && !abortController.signal.aborted) {
            log(`Scroll manually stopped by user (total ${scrollCount} attempts).`, 'warn');
        } else if (scrollCount >= MAX_SCROLL_ATTEMPTS) {
            log(`Reached maximum scroll attempts limit (${MAX_SCROLL_ATTEMPTS}). Some content may be missing.`, 'warn');
        } else if (reachedEnd) {
            log(`Scroll completed successfully after ${scrollCount} attempts. All conversation content captured.`, 'success');
        }

        // Final collection passes to ensure no content was missed
        // These additional passes capture any content that might have been loaded during scrolling
        log("Performing final collection passes to ensure completeness...");

        // Pass 1: Top of conversation
        scroller.scrollTop = 0;
        await delay(FINAL_COLLECTION_DELAY_MS);
        await extractDataIncremental_AiStudio(desiredMode);

        // Pass 2: Middle of conversation
        scroller.scrollTop = scroller.scrollHeight / 2;
        await delay(FINAL_COLLECTION_DELAY_MS);
        await extractDataIncremental_AiStudio(desiredMode);

        // Pass 3: Bottom of conversation
        scroller.scrollTop = scroller.scrollHeight;
        await delay(FINAL_COLLECTION_DELAY_MS);
        await extractDataIncremental_AiStudio(desiredMode);

        log(`Final data collection complete. Total records: ${collectedData.size}`, 'success');
        isScrolling = false;
        return true;
    }

    /**
     * Formats collected conversation data and triggers the download of a Markdown file.
     * Sorts data by DOM order, formats user prompts and AI responses with proper Markdown
     * structure, and generates a downloadable .md file with timestamp.
     * @returns {boolean} - True if the file was successfully created and downloaded, false otherwise.
     */
    function formatAndTriggerDownload() {
        log(`Processing ${collectedData.size} records and generating file...`);
        const finalTurnsInDom = document.querySelectorAll('ms-chat-turn');
        let sortedData = [];

        // Sort by DOM order - now using the turn elements as keys
        finalTurnsInDom.forEach(turnNode => {
            if (collectedData.has(turnNode)) {
                sortedData.push(collectedData.get(turnNode));
            }
        });

        log(`Final export: ${sortedData.length} records found for export`);

        if (sortedData.length === 0) {
            log('No valid records found for export!', 'error');
            alert('After scrolling, no chat records were found for export. Please check the console for details.');
            return false;
        }

        let fileContent = 'Google AI Studio Chat Records (Auto-Scroll Capture)\n';
        fileContent += '=========================================\n\n';
        fileContent += `Exported: ${new Date().toLocaleString()}\n`;
        fileContent += `Total Records: ${sortedData.length}\n\n`;

        sortedData.forEach(item => {
            let parts = [];

            if (INCLUDE_USER_MESSAGES && item.type === 'user' && item.userText) {
                parts.push(`--- User ---\n${item.userText}`);
            }

            // Include AI thinking output
            if (INCLUDE_AI_THINKING && (item.type === 'model_thought' || item.type === 'model_thought_reply')) {
                if (item.thoughtText) {
                    parts.push(`--- AI Thinking ---\n${item.thoughtText}`);
                }
            }

            // Include AI response
            if (INCLUDE_AI_RESPONSES && (item.type === 'model_reply' || item.type === 'model_thought_reply')) {
                if (item.responseText) {
                    parts.push(`--- AI Response ---\n${item.responseText}`);
                }
            }

            if (parts.length > 0) {
                const turnContent = parts.join('\n\n');
                fileContent += turnContent + '\n\n------------------------------\n\n';
            }
        });

        // Add warning if no content was exported due to filters
        if (sortedData.length > 0 && !fileContent.includes('---')) {
            log('Warning: All content was filtered out based on configuration settings', 'warn');
            alert('No content to export based on current filter settings. Please check INCLUDE_* configuration options.');
            return false;
        }

        // Clean up trailing separator
        fileContent = fileContent.replace(/\n\n------------------------------\n\n$/, '\n').trim();

        try {
            const blob = new Blob([fileContent], { type: 'text/markdown;charset=utf-8' });
            const link = document.createElement('a');
            const url = URL.createObjectURL(blob);
            link.href = url;
            link.download = `${EXPORT_FILENAME_PREFIX}${getCurrentTimestamp()}.md`;
            document.body.appendChild(link);
            link.click();
            document.body.removeChild(link);
            URL.revokeObjectURL(url);
            log("File successfully generated and download triggered.", 'success');
            return true;
        } catch (e) {
            log(`File creation failed: ${e.message}`, 'error');
            alert("Error creating download file: " + e.message);
            return false;
        }
    }

    /**
     * Aborts the currently running extraction process.
     */
    function abortExtraction() {
        if (abortController) {
            abortController.abort();
        }
        isScrolling = false;
        log('Extraction manually aborted by user.', 'warn');
        setButtonState('IDLE');
    }

    /**
     * Main orchestrator function for the export process.
     * Handles the complete workflow including Raw Mode toggle, scrolling, data extraction,
     * and file download. Ensures proper error handling and UI state management.
     * @async
     */
    async function handleScrollExtraction() {
        abortController = new AbortController();
        setButtonState('WORKING');

        const currentMode = detectCurrentMode();
        const desiredMode = PREFER_RAW_MODE ? 'raw' : 'rendered';
        let needsToggle = (currentMode !== desiredMode);
        let modeWasToggled = false;

        try {
            if (needsToggle) {
                log(`Switching from ${currentMode} to ${desiredMode} mode...`);
                modeWasToggled = await toggleRawMode();
                if (!modeWasToggled) {
                    throw new Error(`Failed to switch to ${desiredMode} Mode.`);
                }
            } else {
                log(`Already in desired ${desiredMode} mode, no toggle needed.`);
            }

            if (abortController.signal.aborted) throw new Error("Aborted after mode switch.");

            const scrollSuccess = await autoScroll_AiStudio('down');
            if (scrollSuccess === false && !abortController.signal.aborted) {
                throw new Error("Scrolling process failed or was incomplete.");
            }

            if (abortController.signal.aborted) throw new Error("Aborted during scroll.");

            log('Scroll completed, preparing final data processing...');
            await delay(FINAL_CAPTURE_DELAY_MS);
            await extractDataIncremental_AiStudio(desiredMode);
            await delay(POST_EXTRACTION_DELAY_MS);

            const downloadSuccess = formatAndTriggerDownload();
            if (!downloadSuccess) {
                 throw new Error("File generation or download failed.");
            }

            setButtonState('SUCCESS');

        } catch (error) {
            if (!abortController.signal.aborted) {
                log(`Error during processing: ${error.message}`, 'error');
                // alert(`An error occurred: ${error.message}`);
                setButtonState('ERROR');
            } else {
                log('Process was aborted, cleaning up.', 'warn');
            }
        } finally {
            if (modeWasToggled) {
                log(`Switching back to ${currentMode} mode...`);
                await toggleRawMode();
            }
            isScrolling = false;
        }
    }

    //================================================================================
    // UI CREATION - User interface components and interactions
    //================================================================================

    /**
     * Sets the visual state of the export button.
     * @param {'IDLE'|'WORKING'|'SUCCESS'|'ERROR'} state The target state.
     */
    function setButtonState(state) {
        exportButtonState = state;
        if (!exportButton || !exportIcon) return;

        // Remove existing color classes
        exportButton.classList.remove('success', 'error');

        switch (state) {
            case 'IDLE':
                exportIcon.textContent = 'download';
                exportButton.title = 'Export Chat to Markdown';
                exportButton.disabled = false;
                break;
            case 'WORKING':
                exportIcon.textContent = 'cancel';
                exportIcon.style.color = '#d93025'; // Red color for stop
                exportButton.title = 'Stop Export';
                exportButton.disabled = false;
                break;
            case 'SUCCESS':
                exportIcon.textContent = 'check_circle';
                exportIcon.style.color = '#1e8e3e'; // Green color for success
                exportButton.title = 'Export Successful!';
                exportButton.disabled = true;
                setTimeout(() => setButtonState('IDLE'), SUCCESS_RESET_TIMEOUT_MS);
                break;
            case 'ERROR':
                exportIcon.textContent = 'error';
                exportIcon.style.color = '#d93025'; // Red color for error
                exportButton.title = 'Export Failed!';
                exportButton.disabled = true;
                setTimeout(() => setButtonState('IDLE'), ERROR_RESET_TIMEOUT_MS);
                break;
        }

        // Reset color for non-transient states
        if (state === 'IDLE') {
            exportIcon.style.color = ''; // Use default color
        }
    }

    /**
     * Creates and initializes the export button UI element with Google-styled appearance.
     * Positions the button fixed at the bottom-left of the page and attaches the export handler.
     */
    function createUI() {
        const toolbarRight = document.querySelector('ms-toolbar .toolbar-right');
        if (!toolbarRight || document.getElementById('export-button-container')) {
            return;
        }

        log('Toolbar found, creating export button...');

        // Create container for the button group
        const buttonContainer = document.createElement('div');
        buttonContainer.id = 'export-button-container';
        buttonContainer.style.cssText = 'display: flex; align-items: center; margin: 0 4px; position: relative; z-index: 2147483647;';

        // Create main export button
        exportButton = document.createElement('button');
        exportButton.id = 'aistudio-export-button';
        exportButton.setAttribute('ms-button', '');
        exportButton.setAttribute('variant', 'icon-borderless');
        exportButton.className = 'mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon';

        exportButton.addEventListener('click', () => {
            if (exportButtonState === 'IDLE') {
                handleScrollExtraction();
            } else if (exportButtonState === 'WORKING') {
                abortExtraction();
            }
        });

        exportIcon = document.createElement('span');
        exportIcon.className = 'material-symbols-outlined notranslate ms-button-icon-symbol';
        exportButton.appendChild(exportIcon);

        // Create settings button
        const settingsButton = document.createElement('button');
        settingsButton.id = 'aistudio-settings-button';
        settingsButton.title = 'Export Settings';
        settingsButton.setAttribute('ms-button', '');
        settingsButton.setAttribute('variant', 'icon-borderless');
        settingsButton.className = 'mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon';

        const settingsIcon = document.createElement('span');
        settingsIcon.className = 'material-symbols-outlined notranslate ms-button-icon-symbol';
        settingsIcon.textContent = 'settings';
        settingsButton.appendChild(settingsIcon);

        // Append buttons to container
        buttonContainer.appendChild(exportButton);
        buttonContainer.appendChild(settingsButton);

        // Create settings panel with ALTERNATIVE STRATEGY 1: position: fixed
        // This moves the panel out of the toolbar hierarchy entirely
        settingsPanel = document.createElement('div');
        settingsPanel.id = 'aistudio-settings-panel';
        settingsPanel.style.cssText = `
            position: fixed;
            top: 52px;
            right: 16px;
            width: 240px;
            background: #2d2e30;
            border: 1px solid #5f6368;
            border-radius: 8px;
            z-index: 2147483647;
            font-family: 'Google Sans', sans-serif;
            font-size: 14px;
            display: none;
            flex-direction: column;
            box-sizing: border-box;
            padding-top: 16px;
            padding-right: 16px;
            padding-bottom: 16px;
            padding-left: 16px;
            box-shadow: 0 8px 16px rgba(0,0,0,0.3);
            color: #e8eaed;
            isolation: isolate;
            transform: translateZ(0);
        `;

        function createCheckbox(label, configVar, callback) {
            const wrapper = document.createElement('div');
            wrapper.style.cssText = 'display: flex; align-items: center; margin-bottom: 10px;';
            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = configVar;
            // Generate a unique ID that won't conflict between chat sessions
            const timestamp = Date.now();
            const randomId = Math.floor(Math.random() * 10000);
            checkbox.id = `export-checkbox-${label.replace(/\s+/g, '-')}-${timestamp}-${randomId}`;
            const labelEl = document.createElement('label');
            labelEl.textContent = label;
            labelEl.style.marginLeft = '10px';
            labelEl.style.cursor = 'pointer'; // Visual hint that it's clickable
            labelEl.setAttribute('for', checkbox.id);

            // Store a reference to the callback for both label and checkbox
            const handleChange = (checked) => {
                checkbox.checked = checked;
                callback(checked);
            };

            // Make the label directly toggle the checkbox
            labelEl.addEventListener('click', (event) => {
                event.preventDefault(); // Prevent default behavior
                handleChange(!checkbox.checked);
            });

            // Also ensure the checkbox itself works
            checkbox.addEventListener('change', (event) => callback(event.target.checked));

            wrapper.appendChild(checkbox);
            wrapper.appendChild(labelEl);
            settingsPanel.appendChild(wrapper);
            return checkbox;
        }

        preferRawModeCheckbox = createCheckbox('Prefer Raw Mode', PREFER_RAW_MODE, (val) => PREFER_RAW_MODE = val);
        includeUserMessagesCheckbox = createCheckbox('Include User Messages', INCLUDE_USER_MESSAGES, (val) => INCLUDE_USER_MESSAGES = val);
        includeAiResponsesCheckbox = createCheckbox('Include AI Responses', INCLUDE_AI_RESPONSES, (val) => INCLUDE_AI_RESPONSES = val);
        includeAiThinkingCheckbox = createCheckbox('Include AI Thinking', INCLUDE_AI_THINKING, (val) => INCLUDE_AI_THINKING = val);

        // Remove margin-bottom from the last checkbox to prevent extra space at the bottom
        const lastWrapper = settingsPanel.lastElementChild;
        if (lastWrapper) {
            lastWrapper.style.marginBottom = '0';
        }

        // ALTERNATIVE STRATEGY 1: Append panel to body instead of buttonContainer
        // This completely removes it from the toolbar stacking context
        document.body.appendChild(settingsPanel);

        // Update position dynamically based on button location
        const updatePanelPosition = () => {
            const buttonRect = settingsButton.getBoundingClientRect();
            settingsPanel.style.top = `${buttonRect.bottom + 4}px`;
            settingsPanel.style.right = `${window.innerWidth - buttonRect.right}px`;
        };

        // Settings button logic
        settingsButton.addEventListener('click', (event) => {
            event.stopPropagation();
            const isHidden = settingsPanel.style.display === 'none' || settingsPanel.style.display === '';

            if (isHidden) {
                updatePanelPosition(); // Update position before showing
            }

            settingsPanel.style.display = isHidden ? 'flex' : 'none';
        });

        // Update position on scroll/resize
        window.addEventListener('scroll', () => {
            if (settingsPanel.style.display === 'flex') {
                updatePanelPosition();
            }
        });
        window.addEventListener('resize', () => {
            if (settingsPanel.style.display === 'flex') {
                updatePanelPosition();
            }
        });

        // Close panel on outside click
        document.addEventListener('click', (event) => {
            if (!settingsPanel.contains(event.target) && !settingsButton.contains(event.target)) {
                settingsPanel.style.display = 'none';
            }
        });

        // Inject into toolbar
        const moreButton = toolbarRight.querySelector('button[iconname="more_vert"]');
        if (moreButton) {
            toolbarRight.insertBefore(buttonContainer, moreButton);
        } else {
            toolbarRight.appendChild(buttonContainer);
        }

        log("Toolbar UI initialization complete.", 'success');
        setButtonState('IDLE'); // Set the initial state of the button
    }

    //================================================================================
    // MAIN EXECUTION - Script initialization and startup
    //================================================================================

    /**
     * Main entry point function that initializes the AI Studio Export script.
     * Creates the UI and sets up the script for user interaction.
     */
    function initialize() {
        log("Initializing AI Studio Export...");
        // Initial attempt to create the UI in case the toolbar is already present.
        createUI();

        // Set up an observer to re-create the UI if the toolbar is ever re-rendered
        const observer = new MutationObserver((mutationsList) => {
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList') {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === 1 && (node.tagName === 'MS-TOOLBAR' || node.querySelector('ms-toolbar'))) {
                            createUI();
                            return; // We found the toolbar, no need to keep searching
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
        log("MutationObserver is now watching for toolbar changes.");
    }

    initialize();

})();