Gemini Conversation Delete Shortcut

Deletes the current Gemini conversation with a keyboard shortcut or button, provides a Tampermonkey menu command and a Ctrl+Shift+? shortcut to show script status, Ctrl+Shift+S to click the final action button, and a help menu. Uses MutationObserver for UI tracking.

目前為 2025-05-27 提交的版本,檢視 最新版本

// ==UserScript==
// @name              Gemini Conversation Delete Shortcut
// @namespace         https://greasyfork.org/ja/scripts/533285-gemini-conversation-delete-shortcut
// @version           1.6.9
// @description       Deletes the current Gemini conversation with a keyboard shortcut or button, provides a Tampermonkey menu command and a Ctrl+Shift+? shortcut to show script status, Ctrl+Shift+S to click the final action button, and a help menu. Uses MutationObserver for UI tracking.
// @author            Takashi Sasasaki
// @license           MIT
// @homepageURL       https://x.com/TakashiSasaki
// @supportURL        https://greasyfork.org/ja/scripts/533285-gemini-conversation-delete-shortcut
// @match             https://gemini.google.com/app/*
// @match             https://gemini.google.com/app
// @icon              https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png
// @grant             GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // --- Utility to check if an element is visible ---
    function isElementVisible(el) {
        if (!el) return false;
        const style = window.getComputedStyle(el);
        if (style.display === 'none') return false;
        if (style.visibility === 'hidden') return false;
        if (style.opacity === '0') return false;
        return el.offsetParent !== null;
    }

    // --- Function to show script status (for menu command and shortcut) ---
    function showStatusDialog() {
        const headers = document.querySelectorAll('div.response-container-header');
        const currentUrl = window.location.href;
        const menuButtonElement = document.querySelector(SELECTOR_MENU_BUTTON);
        const menuButtonIsDomPresent = !!menuButtonElement;
        const menuButtonIsCurrentlyVisible = isElementVisible(menuButtonElement);
        alert(
            `Gemini Conversation Delete Shortcut is active (version 1.6.9).\n` +
            `URL: ${currentUrl}\n` +
            `Found ${headers.length} elements matching div.response-container-header.\n` +
            `Conversation actions menu button (${SELECTOR_MENU_BUTTON}):\n` +
            `  - In DOM: ${menuButtonIsDomPresent}\n` +
            `  - Visible: ${menuButtonIsCurrentlyVisible}\n` +
            `Using MutationObserver for UI changes.`
        );
        console.log(`Delete Shortcut Status: URL=${currentUrl}, Found ${headers.length} headers. Menu button DOM present: ${menuButtonIsDomPresent}, Visible: ${menuButtonIsCurrentlyVisible}. Using MutationObserver.`);
    }

    /**
     * showHelp - Display a simple help of available shortcuts.
     */
    function showHelp() {
        const helpText = [
            'Gemini Conversation Delete Shortcut Help:',
            'Ctrl+Shift+Backspace → Start deletion sequence (open menu, click Delete, focus Confirm)',
            'Ctrl+Shift+S → Click final action button (e.g., New Chat)',
            'Ctrl+Shift+? → Show script status',
            '🗑️ Yellow button → Manual deletion trigger next to the More menu'
        ].join('\n');
        alert(helpText);
    }

    // --- Register Tampermonkey menu commands ---
    GM_registerMenuCommand('Show delete shortcut status', showStatusDialog);
    GM_registerMenuCommand('Show shortcuts help', showHelp, 'H');

    // --- Configuration ---
    const SHORTCUT_KEY_CODE = 'Backspace';
    const USE_CTRL_KEY = true;
    const USE_SHIFT_KEY = true;
    const USE_ALT_KEY = false;
    const USE_META_KEY = false;

    const SELECTOR_MENU_BUTTON = '[data-test-id="conversation-actions-button"]';
    const SELECTOR_DELETE_BUTTON_IN_MENU = '[data-test-id="delete-button"]';
    const SELECTOR_CONFIRM_BUTTON_IN_DIALOG = '[data-test-id="confirm-button"]';
    const SELECTOR_FINAL_BUTTON = '#app-root > main > div > button';

    const WAIT_AFTER_MENU_CLICK = 100;
    const WAIT_AFTER_DELETE_CLICK = 100;
    const POLLING_INTERVAL = 50;
    const MAX_POLLING_TIME = 3000;
    const MAX_WIDTH_FOR_AUTOMATION = 960;

    // --- Utility functions ---
    function sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async function pollForElement(selector, maxTime, interval) {
        const startTime = Date.now();
        while (Date.now() - startTime < maxTime) {
            const element = document.querySelector(selector);
            if (element) {
                return element;
            }
            await sleep(interval);
        }
        return null;
    }

    // --- Main automation sequence to delete conversation up to confirmation ---
    async function performAutomationSequence() {
        if (window.innerWidth > MAX_WIDTH_FOR_AUTOMATION) {
            console.warn(`Automation sequence aborted: window width (${window.innerWidth}px) exceeds limit (${MAX_WIDTH_FOR_AUTOMATION}px). Intended for mobile-like views.`);
            return;
        }
        try {
            const menuButton = document.querySelector(SELECTOR_MENU_BUTTON);
            if (!menuButton || !isElementVisible(menuButton)) {
                console.error(`Menu button (${SELECTOR_MENU_BUTTON}) not found or not visible.`);
                alert('Conversation actions menu not found or hidden. Cannot proceed with deletion.');
                throw new Error('Menu button not found or not visible');
            }
            menuButton.click();
            await sleep(WAIT_AFTER_MENU_CLICK);

            const deleteBtn = await pollForElement(SELECTOR_DELETE_BUTTON_IN_MENU, MAX_POLLING_TIME, POLLING_INTERVAL);
            if (!deleteBtn || !isElementVisible(deleteBtn)) {
                throw new Error('Delete button not found or not visible');
            }
            deleteBtn.click();
            await sleep(WAIT_AFTER_DELETE_CLICK);

            const confirmBtn = await pollForElement(SELECTOR_CONFIRM_BUTTON_IN_DIALOG, MAX_POLLING_TIME, POLLING_INTERVAL);
            if (!confirmBtn || !isElementVisible(confirmBtn)) {
                throw new Error('Confirm button not found or not visible');
            }

            console.log(`Confirm button found. Focusing and highlighting.`);
            confirmBtn.focus({ preventScroll: false });
            confirmBtn.style.backgroundColor = 'lightgreen';
            confirmBtn.style.border = '3px solid green';
            confirmBtn.style.color = 'black';
            confirmBtn.style.outline = '2px dashed darkgreen';

            console.log('Confirmation button is highlighted. Press Enter or click to confirm deletion. Use Ctrl+Shift+S to click any subsequent final button.');

        } catch (err) {
            console.error('Automation error:', err.message);
        }
    }

    // --- Keyboard shortcut listener ---
    document.addEventListener('keydown', event => {
        if (event.code === SHORTCUT_KEY_CODE &&
            event.ctrlKey === USE_CTRL_KEY &&
            event.shiftKey === USE_SHIFT_KEY &&
            event.altKey === USE_ALT_KEY &&
            event.metaKey === USE_META_KEY) {
            event.preventDefault();
            event.stopPropagation();
            const menuButtonElement = document.querySelector(SELECTOR_MENU_BUTTON);
            if (!isElementVisible(menuButtonElement)) {
                console.log('Shortcut used but menu button not visible. Aborting.');
                alert('Conversation actions menu (︙) not found or not displayed. Cannot delete.');
                return;
            }
            if (window.innerWidth > MAX_WIDTH_FOR_AUTOMATION) {
                console.warn('Shortcut used but window width exceeds limit.');
            }
            performAutomationSequence();
        }
        else if (event.ctrlKey && event.shiftKey && event.key === '?') {
            event.preventDefault();
            event.stopPropagation();
            showStatusDialog();
        }
        else if (event.ctrlKey && event.shiftKey && (event.key === 'S' || event.key === 's')) {
            event.preventDefault();
            event.stopPropagation();
            const finalButton = document.querySelector(SELECTOR_FINAL_BUTTON);
            if (finalButton && isElementVisible(finalButton)) {
                finalButton.click();
            } else {
                console.warn('Final action button not found or not visible.');
                alert('Final action button not found or not displayed.');
            }
        }
    }, true);

    // --- Manual trigger button insertion ---
    function insertManualTriggerButton() {
        const menuButtonElement = document.querySelector(SELECTOR_MENU_BUTTON);
        const isMenuVisible = isElementVisible(menuButtonElement);
        const existingButtons = document.querySelectorAll('.delete-shortcut-button');

        if (isMenuVisible) {
            const wrapperSelector = 'div.menu-button-wrapper';
            let targetWrapper = null;

            if (menuButtonElement.closest(wrapperSelector)) {
                targetWrapper = menuButtonElement.closest(wrapperSelector);
            } else if (menuButtonElement.parentElement && menuButtonElement.parentElement.classList.contains('menu-button-wrapper')) {
                targetWrapper = menuButtonElement.parentElement;
            }

            if (targetWrapper) {
                const existing = targetWrapper.parentNode.querySelector('.delete-shortcut-button');
                if (existing) {
                    existing.remove();
                }
                const btn = document.createElement('button');
                btn.className = 'delete-shortcut-button';
                btn.title = 'Delete conversation (Ctrl+Shift+Backspace)';
                btn.textContent = '🗑️';
                btn.style.marginLeft = '8px';
                btn.style.padding = '4px';
                btn.style.border = '1px solid red';
                btn.style.background = 'yellow';
                btn.style.cursor = 'pointer';
                btn.style.zIndex = '9999';
                btn.addEventListener('click', e => {
                    e.preventDefault();
                    e.stopPropagation();
                    performAutomationSequence();
                });
                targetWrapper.parentNode.insertBefore(btn, targetWrapper.nextSibling);
            } else {
                existingButtons.forEach(btn => btn.remove());
            }
        } else {
            existingButtons.forEach(btn => btn.remove());
        }
    }

    // --- Observe DOM changes and debounce ---
    let observerDebounceTimeout;
    const observer = new MutationObserver(() => {
        clearTimeout(observerDebounceTimeout);
        observerDebounceTimeout = setTimeout(insertManualTriggerButton, 150);
    });
    observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });

    // --- Initial manual button insertion ---
    setTimeout(insertManualTriggerButton, 500);

})();