Greasy Fork is available in English.

Copilot Bulk Delete

Bulk delete chats in Copilot

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         Copilot Bulk Delete
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Bulk delete chats in Copilot
// @author       php
// @match        https://copilot.microsoft.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // ==========================================
    // 1. 配置設定 (Configuration)
    // ==========================================
    const CONFIG = {
        delayMs: 200, // 自動化點擊的間隔時間
        selectors: {
            // 對話列最外層容器
            chatContainer: 'div[role="link"]',
            // 更多選項按鈕 (滑鼠移入後出現)
            moreButton: 'button[id^="conversation-options-"]',
            // 下拉選單中的「刪除」按鈕
            deleteMenuItem: "div[role='menu'] > div > button[role='menuitem']:last-child",
            // 彈出視窗中的「確認刪除」按鈕
            confirmDeleteBtn: `div[data-tabster*='"modalizer":{"id":"modal-'][data-tabster*='"isOthersAccessible":false,"isAlwaysAccessible":true,"isTrapped":true}']:nth-child(2) button:nth-child(1)`
        }
    };

    // ==========================================
    // 2. 建立懸浮的「批次刪除」控制面板 (外層容器)
    // ==========================================
    const batchDeleteContainer = document.createElement('div');
    batchDeleteContainer.id = 'batch-delete-island';
    // 樣式設定:黑色膠囊背景
    batchDeleteContainer.style.cssText = `
        position: fixed;
        bottom: 30px;
        left: 50%;
        transform: translateX(-50%);
        background-color: #202124;
        padding: 8px 12px 8px 24px;
        border-radius: 32px;
        display: none; /* 預設隱藏 */
        align-items: center;
        gap: 16px;
        box-shadow: 0 8px 24px rgba(0,0,0,0.3);
        z-index: 9999;
        transition: all 0.3s ease;
        font-family: sans-serif;
    `;

    // 建立顯示數量的文字
    const countDisplay = document.createElement('span');
    countDisplay.style.cssText = `
        color: white;
        font-size: 14px;
        font-weight: 500;
    `;

    // 建立真正的刪除按鈕 (紅色區域)
    const batchDeleteBtn = document.createElement('button');
    batchDeleteBtn.innerHTML = 'Confirm Bulk Delete 🗑️';
    batchDeleteBtn.style.cssText = `
        background-color: #f28b82;
        color: #202124;
        border: none;
        padding: 8px 20px;
        border-radius: 20px;
        cursor: pointer;
        font-weight: bold;
        font-size: 14px;
        transition: background-color 0.2s;
    `;
    batchDeleteBtn.onmouseover = () => batchDeleteBtn.style.backgroundColor = '#ee675c';
    batchDeleteBtn.onmouseout = () => batchDeleteBtn.style.backgroundColor = '#f28b82';

    batchDeleteContainer.appendChild(countDisplay);
    batchDeleteContainer.appendChild(batchDeleteBtn);
    document.body.appendChild(batchDeleteContainer);

    // ==========================================
    // 3. 核心邏輯:輔助函式
    // ==========================================

    // 延遲函式 (保留做為極短的緩衝使用)
    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));

    // 新增:事件驅動的 DOM 等待函式 (Event-Driven Wait)
    async function waitFor(predicate, timeout = 3000, context = 'condition') {
        const check = () => {
            try { return predicate(); } catch (e) { return false; }
        };

        const initial = check();
        if (initial) return initial;

        return new Promise((resolve) => {
            const observer = new MutationObserver(() => {
                const result = check();
                if (result) {
                    observer.disconnect();
                    resolve(result);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true,
                attributes: true
            });

            setTimeout(() => {
                observer.disconnect();
                console.warn(`Timeout waiting for: ${context}`);
                resolve(null);
            }, timeout);
        });
    }

    // 模擬滑鼠移入 (Hover)
    const simulateHover = (element) => {
        const events = ['pointerover', 'mouseover', 'mouseenter'];
        events.forEach(type => {
            const event = new MouseEvent(type, { bubbles: true, cancelable: true, view: window });
            element.dispatchEvent(event);
        });
    };

    // 更新懸浮面板顯示狀態
    const updateButtonVisibility = () => {
        const checkedBoxes = document.querySelectorAll('.batch-delete-cb:checked');
        const count = checkedBoxes.length;

        // Reset "Select All" checkbox if no items are currently selected
        if (count === 0) {
            const selectAllCb = document.getElementById('select-all-checkbox');
            if (selectAllCb) selectAllCb.checked = false;
        }

        // 控制外層容器的顯示與隱藏
        batchDeleteContainer.style.display = count > 0 ? 'flex' : 'none';

        // 更新計數文字
        countDisplay.innerText = `${count} selected`;
    };

    // Tracks the last clicked checkbox for Shift + Click range selection
    let lastCheckedCb = null;

    // Selects or deselects a range of checkboxes between startCb and endCb
    const selectRange = (startCb, endCb, targetState) => {
        const allCbs = Array.from(document.querySelectorAll('.batch-delete-cb'));
        const startIdx = allCbs.indexOf(startCb);
        const endIdx = allCbs.indexOf(endCb);

        if (startIdx === -1 || endIdx === -1) return;

        const min = Math.min(startIdx, endIdx);
        const max = Math.max(startIdx, endIdx);

        for (let i = min; i <= max; i++) {
            allCbs[i].checked = targetState;
        }
        updateButtonVisibility();
    };

    // ==========================================
    // 4. 將「全選」按鈕注入到側邊欄容器內
    // ==========================================
    const injectSelectAllToSidebar = () => {
        const sidebarContent = document.querySelector('div.grow[data-testid="sidebar-expanded-content"]');

        if (sidebarContent && !document.getElementById('batch-select-all-wrapper')) {
            const wrapper = document.createElement('div');
            wrapper.id = 'batch-select-all-wrapper';
            // 樣式:與對話列保持一致的間距與對齊方式
            wrapper.style.cssText = `
                padding: 12px 16px;
                display: flex;
                align-items: center;
                cursor: pointer;
                user-select: none;
                border-bottom: 1px solid rgba(0,0,0,0.05);
            `;

            // 建立 Checkbox 元素
            const selectAllCheckbox = document.createElement('input');
            selectAllCheckbox.type = 'checkbox';
            selectAllCheckbox.id = 'select-all-checkbox';
            selectAllCheckbox.style.cssText = `
                width: 18px;
                height: 18px;
                cursor: pointer;
                margin-right: 12px;
                flex-shrink: 0;
            `;

            // 建立文字標籤
            const label = document.createElement('span');
            label.innerText = 'Select All';
            label.style.cssText = `
                font-size: 14px;
                color: #5f6368; /* 灰藍色調,符合圖片樣式 */
                font-family: inherit;
            `;

            // 點擊文字也能觸發 Checkbox
            wrapper.addEventListener('click', (e) => {
                // 如果點擊的是 wrapper 本身(非 checkbox),則手動切換狀態
                if (e.target !== selectAllCheckbox) {
                    selectAllCheckbox.checked = !selectAllCheckbox.checked;
                }

                // 執行全選邏輯
                const checkboxes = document.querySelectorAll('.batch-delete-cb');
                checkboxes.forEach(cb => {
                    cb.checked = selectAllCheckbox.checked;
                });

                // 更新主按鈕顯示狀態
                updateButtonVisibility();
            });

            wrapper.appendChild(selectAllCheckbox);
            wrapper.appendChild(label);

            // 將整個全選控制項插入到側邊欄的最上方
            sidebarContent.prepend(wrapper);
        }
    };

    // ==========================================
    // 5. 核心邏輯:UI 注入
    // ==========================================
    // 修改後的 UI 注入邏輯
    const injectCheckboxes = () => {
        const chatContainers = document.querySelectorAll(CONFIG.selectors.chatContainer);

        chatContainers.forEach(container => {
            // 避免重複加入
            if (container.querySelector('.batch-delete-cb')) return;

            // --- 修正佈局:確保容器內元素橫向排列並置中 ---
            container.style.display = 'flex';
            container.style.alignItems = 'center';
            container.style.flexDirection = 'row'; // 強制橫向
            // -------------------------------------------

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.className = 'batch-delete-cb';

            // 微調 Checkbox 樣式,增加間距並確保它不會縮小
            checkbox.style.cssText = `
                margin-right: 10px;
                margin-left: 5px;
                cursor: pointer;
                width: 16px;
                height: 16px;
                flex-shrink: 0; /* 確保 Checkbox 不會因為文字太長而被壓扁 */
                z-index: 10;
            `;

            // 避免點擊 Checkbox 時觸發進入對話的事件 (Supports Shift + Click range selection)
            checkbox.addEventListener('click', (e) => {
                e.stopPropagation();

                if (e.shiftKey && lastCheckedCb && document.body.contains(lastCheckedCb)) {
                    selectRange(lastCheckedCb, checkbox, checkbox.checked);
                } else {
                    updateButtonVisibility();
                }

                lastCheckedCb = checkbox;
            });

            // 將 Checkbox 插入到容器的最前方
            container.insertBefore(checkbox, container.firstChild);
        });
    };

    // 使用 MutationObserver 監聽 DOM 變化,確保動態載入的對話也能加上 Checkbox
    const observer = new MutationObserver(() => {
        injectCheckboxes();
        injectSelectAllToSidebar();
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // ==========================================
    // 6. 核心邏輯:執行批次刪除
    // ==========================================
    batchDeleteBtn.addEventListener('click', async () => {
        const checkedBoxes = document.querySelectorAll('.batch-delete-cb:checked');

        // Confirm before starting deletion
        if (checkedBoxes.length === 0 || !confirm(`Delete ${checkedBoxes.length} chats?`)) return;

        // 避免使用者重複點擊
        batchDeleteBtn.disabled = true;
        batchDeleteBtn.style.backgroundColor = '#ccc';
        batchDeleteBtn.innerText = 'Deleting... Please wait';

        for (const checkbox of checkedBoxes) {
            try {
                // 找到 Checkbox 所在的對話容器
                const container = checkbox.closest(CONFIG.selectors.chatContainer);
                if (!container) continue;

                // 步驟 1: 模擬滑鼠移入對話容器,觸發「更多」按鈕顯示
                simulateHover(container);

                // 動態等待「更多」按鈕出現在 DOM 中,出現後立刻點擊
                const moreBtn = await waitFor(() => container.querySelector(CONFIG.selectors.moreButton), 2000, 'More Button');
                if (!moreBtn) {
                    console.warn('Cannot find the "More" button');
                    continue;
                }
                moreBtn.click();

                // 步驟 2: 動態等待下拉選單的「刪除」按鈕出現,出現後立刻點擊
                const deleteMenuBtn = await waitFor(() => document.querySelector(CONFIG.selectors.deleteMenuItem), 2000, 'Delete Menu Item');
                if (!deleteMenuBtn) {
                    console.warn('Cannot find "Delete" button in dropdown menu');
                    continue;
                }
                deleteMenuBtn.click();

                // 步驟 3: 動態等待彈出視窗的「確認刪除」按鈕出現,出現後立刻點擊
                const confirmBtn = await waitFor(() => document.querySelector(CONFIG.selectors.confirmDeleteBtn), 2000, 'Confirm Delete Button');
                if (!confirmBtn) {
                    console.warn('Cannot find "Confirm Delete" button in pop-up modal');
                    continue;
                }
                confirmBtn.click();

                // 步驟 4: 動態等待「確認刪除」按鈕從 DOM 消失 (代表彈出視窗已關閉)
                await waitFor(() => !document.querySelector(CONFIG.selectors.confirmDeleteBtn), 3000, 'Modal Dismissal');

                // 保留極短的緩衝,確保 React 狀態更新與網路請求不會與下一個迴圈衝突
                await sleep(50);

            } catch (error) {
                console.error('Deletion error:', error);
            }
        }

        // 執行完畢後恢復按鈕狀態並重新整理介面
        batchDeleteBtn.disabled = false;
        batchDeleteBtn.style.backgroundColor = '#d93025';
        updateButtonVisibility();
        console.log('Deletion Finished! ✅');
    });

})();