Gemini Conversation Delete Shortcut

Deletes the current Gemini conversation on both mobile and desktop layouts. Uses simplified polling and timing; focuses and highlights the confirm button without automatic click on both layouts. On desktop, after confirm is clicked, conditionally opens side nav if its content width ≤72px, without adding duplicate listeners. The confirm button is now scoped under message-dialog.

// ==UserScript==
// @name              Gemini Conversation Delete Shortcut
// @namespace         https://greasyfork.org/ja/scripts/533285-gemini-conversation-delete-shortcut
// @version           1.7.7
// @description       Deletes the current Gemini conversation on both mobile and desktop layouts. Uses simplified polling and timing; focuses and highlights the confirm button without automatic click on both layouts. On desktop, after confirm is clicked, conditionally opens side nav if its content width ≤72px, without adding duplicate listeners. The confirm button is now scoped under message-dialog.
// @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
// @run-at            document-idle
// ==/UserScript==

(function() {
    'use strict';

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

    // --- Show script status dialog ---
    function showStatusDialog() {
        const mobilePresent = !!document.querySelector(SELECTOR_MOBILE_MENU_BUTTON);
        const mobileVisible = isElementVisible(document.querySelector(SELECTOR_MOBILE_MENU_BUTTON));
        const desktopPresent = !!document.querySelector(SELECTOR_DESKTOP_MENU_BUTTON);
        alert(
            `Gemini Conversation Delete Shortcut is active (version 1.7.7).\n\n` +
            `Mobile menu button (${SELECTOR_MOBILE_MENU_BUTTON}): In DOM=${mobilePresent}, Visible=${mobileVisible}\n` +
            `Desktop menu button (${SELECTOR_DESKTOP_MENU_BUTTON}): In DOM=${desktopPresent}`
        );
    }

    // --- Show help dialog ---
    function showHelp() {
        alert(
            'Gemini Conversation Delete Shortcut Help:\n' +
            'Ctrl+Shift+Backspace → Start deletion sequence (open menu, click Delete, focus/confirm)\n' +
            'Ctrl+Shift+S        → Click final action button (e.g., New Chat)\n' +
            'Ctrl+Shift+?        → Show script status\n' +
            '🗑️ Button next to menu → Manual deletion trigger (mobile only)'
        );
    }

    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_MOBILE_MENU_BUTTON   = '[data-test-id="conversation-actions-button"]';
    const SELECTOR_DESKTOP_MENU_BUTTON  = 'conversations-list div.selected button';
    const SELECTOR_DELETE_BUTTON        = '[data-test-id="delete-button"]';
    const SELECTOR_CONFIRM_BUTTON       = 'message-dialog button[data-test-id="confirm-button"]';
    const SELECTOR_FINAL_BUTTON         = '#app-root > main > div > button';
    const SELECTOR_SIDE_NAV             = 'bard-sidenav';
    const SELECTOR_SIDE_NAV_TOGGLE      = "button[data-test-id='side-nav-menu-button']";

    const POLLING_INTERVAL              = 100;   // ms
    const MAX_POLLING_TIME              = 1000;  // ms
    const POST_CONFIRM_DELAY            = 200;   // ms

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

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

    // --- Clear dataset flag when dialog closes (so new listeners can be added next time) ---
    function clearConfirmDatasetOnClose() {
        const confirmBtn = document.querySelector(SELECTOR_CONFIRM_BUTTON);
        if (confirmBtn) {
            delete confirmBtn.dataset.sideNavListenerAdded;
        }
    }

    // Listen for dialog-close clicks (e.g., mat-dialog-close attribute)
    document.body.addEventListener('click', event => {
        if (event.target.closest('[mat-dialog-close]')) {
            clearConfirmDatasetOnClose();
        }
    });

    // --- Main deletion sequence ---
    async function performDeletion() {
        try {
            // 1) Try mobile layout
            const mobileBtn = await pollForElement(SELECTOR_MOBILE_MENU_BUTTON, MAX_POLLING_TIME);
            if (mobileBtn && isElementVisible(mobileBtn)) {
                mobileBtn.click();
                const deleteBtn = await pollForElement(SELECTOR_DELETE_BUTTON, MAX_POLLING_TIME);
                if (!deleteBtn || !isElementVisible(deleteBtn)) throw new Error('Delete not found (mobile)');
                deleteBtn.click();
                const confirmBtn = await pollForElement(SELECTOR_CONFIRM_BUTTON, MAX_POLLING_TIME);
                if (!confirmBtn || !isElementVisible(confirmBtn)) throw new Error('Confirm not found (mobile)');

                // Wait for Angular animation to finish
                await sleep(150);

                // Focus and highlight (use !important to override Angular styles)
                confirmBtn.focus({ preventScroll: false });
                confirmBtn.style.setProperty('background-color', 'lightgreen', 'important');
                confirmBtn.style.setProperty('border', '3px solid green', 'important');
                confirmBtn.style.setProperty('color', 'black', 'important');
                confirmBtn.style.setProperty('outline', '2px dashed darkgreen', 'important');
                // No side-nav logic for mobile
                return;
            }

            // 2) Try desktop layout
            const desktopBtn = await pollForElement(SELECTOR_DESKTOP_MENU_BUTTON, MAX_POLLING_TIME);
            if (desktopBtn) {
                desktopBtn.click();
                const deleteBtn = await pollForElement(`div[role="menu"] button${SELECTOR_DELETE_BUTTON}`, MAX_POLLING_TIME);
                if (!deleteBtn) throw new Error('Delete not found (desktop)');
                deleteBtn.click();
                const confirmBtn = await pollForElement(SELECTOR_CONFIRM_BUTTON, MAX_POLLING_TIME);
                if (!confirmBtn) throw new Error('Confirm not found (desktop)');

                // Wait for Angular animation to finish
                await sleep(150);

                // Focus and highlight (use !important)
                confirmBtn.focus({ preventScroll: false });
                confirmBtn.style.setProperty('background-color', 'lightgreen', 'important');
                confirmBtn.style.setProperty('border', '3px solid green', 'important');
                confirmBtn.style.setProperty('color', 'black', 'important');
                confirmBtn.style.setProperty('outline', '2px dashed darkgreen', 'important');

                // Add side-nav listener only once per dialog instance
                if (!confirmBtn.dataset.sideNavListenerAdded) {
                    confirmBtn.dataset.sideNavListenerAdded = 'true';
                    confirmBtn.addEventListener('click', async () => {
                        // After user clicks confirm, wait and then toggle side-nav if narrow
                        await sleep(POST_CONFIRM_DELAY);
                        const sideNav = document.querySelector(SELECTOR_SIDE_NAV);
                        if (sideNav) {
                            const style = window.getComputedStyle(sideNav);
                            const paddingLeft = parseFloat(style.paddingLeft) || 0;
                            const paddingRight = parseFloat(style.paddingRight) || 0;
                            const contentWidth = sideNav.clientWidth - paddingLeft - paddingRight;
                            if (contentWidth <= 72) {
                                const toggleBtn = document.querySelector(SELECTOR_SIDE_NAV_TOGGLE);
                                if (toggleBtn && toggleBtn.offsetParent !== null) {
                                    toggleBtn.click();
                                }
                            }
                        }
                    }, { once: true });
                }

                return;
            }

            alert('Conversation menu button not found. Cannot delete.');
        } catch (err) {
            console.error('Deletion 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();
            performDeletion();
        }
        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 finalBtn = document.querySelector(SELECTOR_FINAL_BUTTON);
            if (finalBtn && isElementVisible(finalBtn)) {
                finalBtn.click();
            } else {
                alert('Final action button not found.');
            }
        }
    }, true);

    // --- Manual delete button insertion for mobile layout only ---
    function insertMobileButton() {
        const mobileBtn = document.querySelector(SELECTOR_MOBILE_MENU_BUTTON);
        const isVisible = isElementVisible(mobileBtn);
        document.querySelectorAll('.delete-shortcut-button').forEach(b => b.remove());

        if (isVisible) {
            let wrapper = mobileBtn.closest('div.menu-button-wrapper') || mobileBtn.parentElement;
            if (!wrapper || !wrapper.classList.contains('menu-button-wrapper')) return;
            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();
                performDeletion();
            });
            wrapper.parentNode.insertBefore(btn, wrapper.nextSibling);
        }
    }

    let debounce;
    const observer = new MutationObserver(() => {
        clearTimeout(debounce);
        debounce = setTimeout(insertMobileButton, 150);
    });
    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['style', 'class']
    });
    setTimeout(insertMobileButton, 500);

})();