Bulk delete chats in M365 Copilot
// ==UserScript==
// @name M365 Copilot Bulk Delete
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Bulk delete chats in M365 Copilot
// @author php
// @match https://m365.cloud.microsoft/*
// @grant none
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const SELECTORS = {
// Universal Filter: Selects the row ONLY IF it contains the chat menu item button
chatRow: 'div.fai-CopilotNavSubItemGroup div.fui-SplitNavItem:has(button[aria-roledescription="Menu item"])',
// The main button containing the ID
chatBtn: 'button[aria-roledescription="Menu item"]',
// The "More" button (sibling of chatBtn)
moreBtn: '.fai-SplitCopilotNavItem__menuButton',
// Language-proof: Targets the menu item immediately following the visual divider
dropdownDelete: 'div[role="separator"] + div[role="menuitem"]',
// Language-proof: Targets the final confirm button that lacks the restorer attribute
modalConfirm: '.fui-DialogActions button:not([data-tabster])',
// Target for the "Select All" UI
selectAllTarget: 'div.___13wxke1.f1xg1ack.f15twtuk.f19g0ac.fxugw4r',
};
const CONFIG = {
// Active chat detection: attribute
activeAttribute: 'aria-current', // You can set this to an attribute or a CSS class
// Active chat detection: attribute value
activeValue: 'page', // Set to null if you only want to check for the presence of an attribute
// Active chat detection: class
activeClass: null // Or set to a class like 'is-active' if the site uses classes
};
const DELAY = 300;
let selectedIds = new Set();
let isDeleting = false;
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const waitForElement = (selector, timeout = 5000) => {
return new Promise((resolve) => {
const start = Date.now();
const interval = setInterval(() => {
const el = document.querySelector(selector);
if (el || (Date.now() - start) > timeout) {
clearInterval(interval);
resolve(el);
}
}, 100);
});
};
// --- Styles ---
const style = document.createElement('style');
style.innerHTML = `
.bulk-delete-checkbox { margin: 0 8px; cursor: pointer; z-index: 10; flex-shrink: 0; transform: scale(1.1); }
#bulk-delete-bar {
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
background: #202124; color: white; padding: 12px 24px; border-radius: 32px;
display: none; align-items: center; gap: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
z-index: 10000; border: 1px solid #3c4043; transition: all 0.3s ease;
}
#bulk-delete-bar.finished { background: #1e8e3e; border-color: #1e8e3e; }
#bulk-delete-bar button {
background: #f28b82; color: #202124; border: none; padding: 8px 18px;
border-radius: 20px; cursor: pointer; font-weight: bold;
}
#bulk-delete-bar button:hover { background: #ee675c; }
${SELECTORS.chatRow} { display: flex !important; align-items: center !important; }
`;
document.head.appendChild(style);
const actionBar = document.createElement('div');
actionBar.id = 'bulk-delete-bar';
actionBar.innerHTML = `
<span id="selected-count">0 selected</span>
<button id="execute-bulk-delete">Confirm Bulk Delete 🗑️</button>
`;
document.body.appendChild(actionBar);
function updateActionBar(message = null) {
const countSpan = document.getElementById('selected-count');
const deleteBtn = document.getElementById('execute-bulk-delete');
if (message) {
countSpan.innerText = message;
deleteBtn.style.display = 'none';
actionBar.classList.add('finished');
actionBar.style.display = 'flex';
} else {
const count = selectedIds.size;
countSpan.innerText = `${count} selected`;
deleteBtn.style.display = 'inline-block';
actionBar.classList.remove('finished');
actionBar.style.display = count > 0 ? 'flex' : 'none';
}
}
function injectCheckbox(row) {
if (row.querySelector('.bulk-delete-checkbox')) return;
const btn = row.querySelector(SELECTORS.chatBtn);
if (!btn || !btn.id) return;
const threadId = btn.id;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'bulk-delete-checkbox';
if (selectedIds.has(threadId)) checkbox.checked = true;
checkbox.addEventListener('change', (e) => {
if (e.target.checked) selectedIds.add(threadId);
else selectedIds.delete(threadId);
updateActionBar();
});
checkbox.addEventListener('click', (e) => e.stopPropagation());
row.prepend(checkbox);
}
async function injectSelectAll() {
if (isDeleting) return;
const header = document.querySelector(SELECTORS.selectAllTarget);
if (!header || header.querySelector('#select-all-wrapper')) return;
const wrapper = document.createElement('div');
wrapper.id = 'select-all-wrapper';
wrapper.style.cssText = 'display:flex; align-items:center; padding: 8px 16px;';
wrapper.innerHTML = `
<input type="checkbox" id="select-all-chats" class="bulk-delete-checkbox">
<label for="select-all-chats" style="cursor:pointer; font-size: 13px; color: #9aa0a6;">Select All</label>
`;
wrapper.querySelector('#select-all-chats').addEventListener('change', (e) => {
const isChecked = e.target.checked;
document.querySelectorAll(SELECTORS.chatRow).forEach(row => {
const btn = row.querySelector(SELECTORS.chatBtn);
const cb = row.querySelector('.bulk-delete-checkbox');
if (btn && cb) {
cb.checked = isChecked;
if (isChecked) selectedIds.add(btn.id);
else selectedIds.delete(btn.id);
}
});
updateActionBar();
});
header.prepend(wrapper);
}
async function deleteSingleItem(threadId) {
const btn = document.getElementById(threadId);
if (!btn) return;
try {
const row = btn.closest(SELECTORS.chatRow);
const moreBtn = row.querySelector(SELECTORS.moreBtn);
if (!moreBtn) return;
moreBtn.click();
await sleep(DELAY + 300);
const dropdownBtn = await waitForElement(SELECTORS.dropdownDelete);
if (!dropdownBtn) return;
dropdownBtn.click();
await sleep(DELAY + 500);
const confirmBtn = await waitForElement(SELECTORS.modalConfirm);
if (confirmBtn) confirmBtn.click();
await sleep(DELAY + 700);
} catch (err) {
console.error("Copilot Deletion error:", err);
}
}
document.getElementById('execute-bulk-delete').addEventListener('click', async () => {
const itemsToProcess = Array.from(selectedIds)
.map(id => {
const btn = document.getElementById(id);
return { id: id, el: btn };
})
.filter(item => item.el !== null)
.sort((a, b) => {
const isActive = (el) => {
if (CONFIG.activeClass) return el.classList.contains(CONFIG.activeClass);
if (CONFIG.activeAttribute) return el.getAttribute(CONFIG.activeAttribute) === CONFIG.activeValue;
return false;
};
const aIsActive = isActive(a.el);
const bIsActive = isActive(b.el);
if (aIsActive && !bIsActive) return 1;
if (!aIsActive && bIsActive) return -1;
return b.el.getBoundingClientRect().top - a.el.getBoundingClientRect().top;
});
if (!itemsToProcess.length || !confirm(`Delete ${itemsToProcess.length} chats?`)) return;
isDeleting = true;
actionBar.style.display = 'none';
for (const item of itemsToProcess) {
await deleteSingleItem(item.id);
selectedIds.delete(item.id);
}
isDeleting = false;
selectedIds.clear();
updateActionBar("Deletion Finished! ✅");
setTimeout(() => {
actionBar.style.display = 'none';
updateActionBar();
}, 3000);
});
const observer = new MutationObserver(() => {
document.querySelectorAll(SELECTORS.chatRow).forEach(injectCheckbox);
injectSelectAll();
});
observer.observe(document.body, { childList: true, subtree: true });
})();