Greasy Fork is available in English.
Bulk delete chats in Copilot
// ==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! ✅');
});
})();