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, and a Ctrl+Shift+S shortcut to click the final action button. Uses MutationObserver instead of resize listener.

目前为 2025-05-13 提交的版本。查看 最新版本

// ==UserScript==
// @name              Gemini Conversation Delete Shortcut
// @namespace         https://x.com/TakashiSasaki/greasyfork/533285
// @version           1.6.8
// @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, and a Ctrl+Shift+S shortcut to click the final action button. Uses MutationObserver instead of resize listener.
// @author            Takashi Sasasaki
// @license           MIT
// @homepageURL       https://x.com/TakashiSasaki
// @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 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.8).\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.`);
    }

    // --- Register Tampermonkey menu command for status ---
    GM_registerMenuCommand('Show delete shortcut status', showStatusDialog);

    // --- 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 (for deleting conversation up to confirm button) ---
    async function performAutomationSequence() {
        if (window.innerWidth > MAX_WIDTH_FOR_AUTOMATION) {
            console.warn(`Automation sequence aborted: window width (${window.innerWidth}px) exceeds MAX_WIDTH_FOR_AUTOMATION (${MAX_WIDTH_FOR_AUTOMATION}px). This function is intended for narrower (mobile-like) views.`);
            // 幅が広い場合でもアラートは出さずにコンソール警告のみにするか、アラートを出すかは要検討
            // alert(`ウィンドウ幅が ${MAX_WIDTH_FOR_AUTOMATION}px を超えているため、自動化シーケンスは実行されません。`);
            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. Automation sequence cannot proceed.`);
                alert('会話アクションメニューが見つからないか非表示のため、削除処理を実行できません。');
                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 in menu 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 in dialog or not visible');

            console.log(`Confirm button (${SELECTOR_CONFIRM_BUTTON_IN_DIALOG}) 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('The confirmation button is now highlighted. Please press Enter or click it to confirm deletion. Use Ctrl+Shift+S to click any subsequent final button.');

        } catch (err) {
            console.error('Automation error:', err.message);
            // エラー発生時にもユーザーに通知する方が親切かもしれない
            // alert(`自動化シーケンス中にエラーが発生しました: ${err.message}`);
        }
    }

    // --- Keyboard shortcut listener ---
    document.addEventListener('keydown', event => {
        // Conversation Delete Shortcut (Ctrl+Shift+Backspace)
        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 activated for delete, but menu button (${SELECTOR_MENU_BUTTON}) is not visible or not present. Aborting.`);
                alert('会話アクションメニュー(︙)が見つからないか、現在表示されていません。\nそのため、ショートカットキーによる削除処理は実行できません。');
                return;
            }
             // 幅チェックをここでも行う(オプションだが、ユーザー体験としては良いかも)
            if (window.innerWidth > MAX_WIDTH_FOR_AUTOMATION) {
                 console.warn(`Shortcut activated, but window width (${window.innerWidth}px) exceeds limit (${MAX_WIDTH_FOR_AUTOMATION}px). Automation sequence will likely be aborted.`);
                 // 必要ならアラート表示
                 // alert(`現在のウィンドウ幅 (${window.innerWidth}px) は自動化シーケンスの実行上限 (${MAX_WIDTH_FOR_AUTOMATION}px) を超えています。`);
            }
            performAutomationSequence();
        }
        // Show Status Dialog Shortcut (Ctrl+Shift+?)
        else if (event.ctrlKey && event.shiftKey && event.key === '?') {
            event.preventDefault();
            event.stopPropagation();
            console.log('Shortcut activated for status dialog (Ctrl+Shift+?).');
            showStatusDialog();
        }
        // Click Final Button Shortcut (Ctrl+Shift+S)
        else if (event.ctrlKey && event.shiftKey && (event.key === 'S' || event.key === 's')) {
            event.preventDefault();
            event.stopPropagation();
            console.log(`Shortcut activated for clicking final button (Ctrl+Shift+S).`);
            const finalButton = document.querySelector(SELECTOR_FINAL_BUTTON);
            if (finalButton && isElementVisible(finalButton)) {
                console.log(`Final button (${SELECTOR_FINAL_BUTTON}) found and visible. Clicking.`);
                finalButton.click();
            } else {
                console.warn(`Final button (${SELECTOR_FINAL_BUTTON}) not found or not visible for Ctrl+Shift+S shortcut.`);
                alert(`最終処理ボタン(${SELECTOR_FINAL_BUTTON} で指定されるボタン)が見つからないか、現在表示されていません。`);
            }
        }
    }, true);

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

        if (isMenuButtonCurrentlyVisible) {
            // メニューボタンが表示されている場合、削除ボタンを挿入または確認する
            console.log(`Menu button (${SELECTOR_MENU_BUTTON}) found and is visible. Checking/inserting delete shortcut button.`); // ログ変更
            const wrapperSelector = 'div.menu-button-wrapper'; // このセレクタがGeminiのUIで安定しているか要確認
            let targetWrapper = null;

            // メニューボタンを含む、または隣接する可能性のあるラッパーを探す
            if (menuButtonElement.closest(wrapperSelector)) {
                 targetWrapper = menuButtonElement.closest(wrapperSelector);
                 console.log('Found wrapper via closest:', targetWrapper);
            } else if (menuButtonElement.parentElement && menuButtonElement.parentElement.classList.contains('menu-button-wrapper')) {
                 // 古い構造へのフォールバック?(前のコードにあったロジック)
                 targetWrapper = menuButtonElement.parentElement;
                 console.log('Found wrapper via parentElement:', targetWrapper);
            } else {
                 // 他の探し方も試す (例: menuButtonElementの親を辿る)
                 let parent = menuButtonElement.parentElement;
                 while(parent && parent !== document.body) {
                    // `menu-button-wrapper` クラスを持つ要素を探す、または特定の構造を探す
                    // この部分はGeminiのHTML構造に依存するため、調整が必要になる可能性が高い
                    // 例: 特定のクラスを持つ親を探す
                    if(parent.querySelector(SELECTOR_MENU_BUTTON)) {
                        // さらに探索... wrapperらしき要素を見つけるロジック
                    }
                    // この例では単純化のため、見つからないケースとしておく
                    parent = parent.parentElement;
                 }
                 console.log('Could not reliably determine the target wrapper near the menu button.');
            }


            // 適切なラッパーが見つかった場合のみボタンを挿入
            if (targetWrapper) {
                 // ボタンがまだ挿入されていないか確認
                let existingButton = targetWrapper.parentNode.querySelector('.delete-shortcut-button');
                 // 挿入位置が wrapper の隣であるかを確認するロジックを改善
                 // (targetWrapperのすぐ隣にボタンがあるべきか、など配置ルールによる)
                 let shouldInsert = true;
                 if (existingButton && existingButton.previousElementSibling === targetWrapper) {
                     shouldInsert = false; // すでに正しい位置にある
                     console.log('Delete button already exists next to the wrapper.');
                 } else if (existingButton) {
                     // 存在はするが位置が違う場合、一旦削除して再挿入するか、何もしないか
                     console.log('Delete button exists but maybe in the wrong place. Removing old one.');
                     existingButton.remove();
                     shouldInsert = true;
                 }


                if (shouldInsert) {
                     console.log(`Inserting delete button next to wrapper:`, targetWrapper);
                    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', event => {
                        event.preventDefault();
                        event.stopPropagation(); // イベント伝播を止める
                        performAutomationSequence();
                    });
                    // ラッパー要素の直後にボタンを挿入
                    targetWrapper.parentNode.insertBefore(btn, targetWrapper.nextSibling);
                    console.log('Inserted delete button.');
                }
            } else {
                 console.log('No suitable wrapper found to insert the delete button near the menu button.');
                 // ラッパーが見つからない場合、既存のボタンがあれば削除する(孤立防止)
                 existingShortcutButtons.forEach(btn => {
                    console.log('Removing orphaned delete shortcut button as no suitable wrapper was found.');
                    btn.remove();
                });
            }

        } else {
            // メニューボタンが表示されていない場合、既存の削除ボタンがあれば削除する
            console.log(`Menu button (${SELECTOR_MENU_BUTTON}) not found or not visible. Removing any existing delete shortcut buttons.`);
            if (existingShortcutButtons.length > 0) {
                existingShortcutButtons.forEach(button => {
                    button.remove();
                    console.log('Removed an existing delete shortcut button.');
                });
            }
        }
    }

    // --- MutationObserver setup ---
    // debounce処理を追加して、短時間に大量の変更があってもinsertManualTriggerButtonの呼び出しを抑制する
    let observerDebounceTimeout;
    const observer = new MutationObserver(mutationsList => {
         clearTimeout(observerDebounceTimeout);
         observerDebounceTimeout = setTimeout(() => {
            console.log('MutationObserver triggered (debounced), updating delete button status.');
            insertManualTriggerButton();
         }, 150); // 150msのデバウンス時間 (調整可能)
    });

    // --- Start observing DOM changes ---
    // 監視対象はbody全体、子要素の追加削除、サブツリー全体、style/class属性の変更を監視
    const targetNode = document.body;
    const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] };
    observer.observe(targetNode, config);
    console.log('MutationObserver started observing document.body.');

    // --- Initial button check ---
    // スクリプト実行時に一度ボタンの状態をチェック
    console.log('Performing initial check for delete button upon script load.');
    // 少し遅延させてDOMの準備が整うのを待つ(より確実に)
    setTimeout(insertManualTriggerButton, 500); // 500ms待機 (調整可能)

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址