Google AI Studio | Quick Actions Toolbar (Delete + Copy Markdown)

Adds Delete and Copy as Markdown buttons directly to the message hover toolbar

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Google AI Studio | Quick Actions Toolbar (Delete + Copy Markdown)
// @namespace    https://greasyfork.org/en/users/1462137-piknockyou
// @version      2.2
// @author       Piknockyou (vibe-coded)
// @license      AGPL-3.0
// @description  Adds Delete and Copy as Markdown buttons directly to the message hover toolbar
// @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==

(function() {
    'use strict';

    if (window.self !== window.top) return;

    //=============================================================================
    // CONFIGURATION
    //=============================================================================
    const CONFIG = {
        DEBUG: false,
        TOOLBAR_SELECTOR: '.actions.hover-or-edit',
        MENU_CONTAINER_SELECTOR: 'ms-chat-turn-options',

        // Timing (ms)
        SCAN_DEBOUNCE_MS: 50,
        MENU_OPEN_DELAY_MS: 1,
        COPY_FEEDBACK_MS: 1000
    };

    const log = (...args) => CONFIG.DEBUG && console.log('[Quick Actions]', ...args);

    //=============================================================================
    // STYLES
    //=============================================================================
    GM_addStyle(`
        .qa-custom-btn {
            margin: 0 2px;
            cursor: pointer;
        }
        .qa-delete-btn span.material-symbols-outlined {
            color: #ea4335 !important;
        }
        .qa-copy-md-btn span.material-symbols-outlined {
            color: #1a73e8 !important;
        }
    `);

    //=============================================================================
    // BUTTON FACTORY
    //=============================================================================
    function createQuickActionButton(iconName, tooltipText, cssClass) {
        const button = document.createElement('button');
        button.setAttribute('ms-button', '');
        button.setAttribute('variant', 'icon-borderless');
        button.setAttribute('mattooltipposition', 'below');
        button.setAttribute('aria-label', tooltipText);
        button.setAttribute('title', tooltipText);
        button.setAttribute('aria-disabled', 'false');
        button.className = `mat-mdc-tooltip-trigger ms-button-borderless ms-button-icon qa-custom-btn ${cssClass}`;
        button.dataset.qaIcon = iconName;

        const iconSpan = document.createElement('span');
        iconSpan.className = 'material-symbols-outlined notranslate ms-button-icon-symbol';
        iconSpan.setAttribute('aria-hidden', 'true');
        iconSpan.textContent = iconName;

        button.appendChild(iconSpan);
        return button;
    }

    //=============================================================================
    // MENU ACTION HELPER
    //=============================================================================

    /**
     * Click a menu item by icon name — opens menu, finds item, clicks it (synchronous)
     */
    function clickMenuItemByIcon(toolbar, iconName) {
        try {
            const menuButton = toolbar.querySelector(`${CONFIG.MENU_CONTAINER_SELECTOR} button`);
            if (!menuButton) {
                log('Menu button not found');
                return false;
            }

            menuButton.click();

            const menuItems = document.querySelectorAll('.cdk-overlay-container .mat-mdc-menu-content button');
            for (const item of menuItems) {
                const icon = item.querySelector('.material-symbols-outlined');
                if (icon && icon.textContent.trim() === iconName) {
                    item.click();
                    log(`${iconName} clicked`);
                    return true;
                }
            }

            document.body.click();
            log(`Menu item '${iconName}' not found`);
            return false;
        } catch (error) {
            log(`Error clicking '${iconName}':`, error);
            return false;
        }
    }

    function copyMarkdownDirect(toolbar) {
        const success = clickMenuItemByIcon(toolbar, 'markdown_copy');
        if (success) showCopyFeedback(toolbar);
        return success;
    }

    function deleteTurnDirect(toolbar) {
        return clickMenuItemByIcon(toolbar, 'delete');
    }

    /**
     * Show visual feedback for copy action
     */
    function showCopyFeedback(toolbar) {
        const copyBtn = toolbar.querySelector('.qa-copy-md-btn');
        if (!copyBtn) return;

        const icon = copyBtn.querySelector('.material-symbols-outlined');
        const originalIcon = icon.textContent;

        icon.textContent = 'check';
        icon.style.color = '#34a853';

        setTimeout(() => {
            icon.textContent = originalIcon;
            icon.style.color = '';
        }, CONFIG.COPY_FEEDBACK_MS);
    }

    //=============================================================================
    // TOOLBAR ENHANCEMENT
    //=============================================================================
    function enhanceToolbar(toolbar) {
        // Skip if already enhanced
        if (toolbar.querySelector('[data-qa-icon]')) return;

        const menuContainer = toolbar.querySelector(CONFIG.MENU_CONTAINER_SELECTOR);
        if (!menuContainer) {
            log('Menu container not found in toolbar');
            return;
        }

        // Create Delete button
        const deleteButton = createQuickActionButton('delete', 'Delete', 'qa-delete-btn');
        deleteButton.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            deleteTurnDirect(toolbar);
        });

        // Create Copy as Markdown button
        const copyMdButton = createQuickActionButton('markdown_copy', 'Copy as markdown', 'qa-copy-md-btn');
        copyMdButton.addEventListener('click', (event) => {
            event.preventDefault();
            event.stopPropagation();
            copyMarkdownDirect(toolbar);
        });

        // Insert buttons before the menu container
        toolbar.insertBefore(copyMdButton, menuContainer);
        toolbar.insertBefore(deleteButton, menuContainer);

        log('Enhanced toolbar');
    }

    //=============================================================================
    // SCANNING & OBSERVATION
    //=============================================================================
    let scanTimeoutId = null;
    const pendingScanRoots = new Set();

    function scheduleToolbarScan() {
        if (scanTimeoutId !== null) return;

        scanTimeoutId = setTimeout(() => {
            scanTimeoutId = null;

            // Scan only newly-added subtrees when possible; fall back to full document scan.
            const roots = pendingScanRoots.size ? Array.from(pendingScanRoots) : [document];
            pendingScanRoots.clear();

            for (const root of roots) {
                try {
                    if (!root) continue;

                    // Root itself might be a toolbar
                    if (root.matches?.(CONFIG.TOOLBAR_SELECTOR)) {
                        enhanceToolbar(root);
                    }

                    // Root may contain toolbars
                    const toolbars = (root === document)
                        ? document.querySelectorAll(CONFIG.TOOLBAR_SELECTOR)
                        : root.querySelectorAll?.(CONFIG.TOOLBAR_SELECTOR);

                    if (toolbars) {
                        toolbars.forEach(enhanceToolbar);
                    }
                } catch (error) {
                    log('Scan error:', error);
                }
            }
        }, CONFIG.SCAN_DEBOUNCE_MS);
    }

    const domObserver = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (!mutation.addedNodes || mutation.addedNodes.length === 0) continue;

            for (const node of mutation.addedNodes) {
                if (node?.nodeType !== 1) continue; // Elements only
                pendingScanRoots.add(node);
            }
        }

        if (pendingScanRoots.size > 0) {
            scheduleToolbarScan();
        }
    });

    //=============================================================================
    // INITIALIZATION
    //=============================================================================
    function initialize() {
        log('Initializing...');

        // Initial scan for existing toolbars
        scheduleToolbarScan();

        // Start observing for new toolbars
        domObserver.observe(document.body, {
            childList: true,
            subtree: true
        });

        log('Initialization complete');
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();