Bulk archive or delete ChatGPT and Gemini conversations
// ==UserScript==
// @name AI Chat Bulk Manager
// @name:zh-CN AI Chat Bulk Manager
// @namespace http://tampermonkey.net/
// @version 0.4
// @description Bulk archive or delete ChatGPT and Gemini conversations
// @description:zh-CN 批量归档或删除 ChatGPT 和 Gemini 的历史会话
// @author Luo Jiahao
// @homepageURL https://github.com/learnerLj/ai-chat-bulk-manager
// @supportURL https://github.com/learnerLj/ai-chat-bulk-manager/issues
// @match https://chatgpt.com/*
// @match https://gemini.google.com/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect chatgpt.com
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const LOG_PREFIX = '[AI Chat Bulk Manager]';
const CONVERSATION_LINK_SELECTOR = 'a[href*="/c/"]';
const GEMINI_BOOT_DELAY_MS = 3500;
const BULK_DELETE_INTERVAL_MS = 100;
const GEMINI_DELETE_POLL_MS = 150;
const MESSAGES = {
zh: {
archiveSelected: '归档选中',
deleteSelected: '删除选中',
stop: '停止',
ready: '就绪',
stopping: '停止中',
cancelled: '已取消',
done: '完成',
stopped: '已停止',
noSelection: '未选择会话',
archiveAction: '归档',
deleteAction: '删除',
detectedConversations: ({ count }) => `检测到 ${count} 条会话`,
processing: ({ current, total }) => `正在处理 ${current}/${total}`,
runningAction: ({ actionLabel, current, total }) => `正在${actionLabel} (${current}/${total})`,
failed: ({ message }) => `失败:${message}`,
confirmAction: ({ actionLabel, count }) => `确认${actionLabel}选中的 ${count} 条会话?`
},
en: {
archiveSelected: 'Archive selected',
deleteSelected: 'Delete selected',
stop: 'Stop',
ready: 'Ready',
stopping: 'Stopping',
cancelled: 'Cancelled',
done: 'Done',
stopped: 'Stopped',
noSelection: 'No conversations selected',
archiveAction: 'archive',
deleteAction: 'delete',
detectedConversations: ({ count }) => `Detected ${count} conversations`,
processing: ({ current, total }) => `Processing ${current}/${total}`,
runningAction: ({ actionLabel, current, total }) => `${actionLabel} (${current}/${total})`,
failed: ({ message }) => `Failed: ${message}`,
confirmAction: ({ actionLabel, count }) => `Confirm ${actionLabel} for ${count} selected conversations?`
}
};
const GEMINI_SELECTORS = {
conversation: [
'gem-nav-list-item[data-test-id="conversation"]',
'div[data-test-id="conversation"]',
'.chat-history-list gem-nav-list-item',
'.chat-history-list a.mat-mdc-list-item'
].join(', '),
historyRoot: [
'.chat-history',
'bard-sidenav',
'side-navigation',
'mat-sidenav',
'[role="navigation"]'
].join(', '),
menuButton: [
'button[data-test-id="actions-menu-button"]',
'button[aria-label*="更多选项"]',
'button[aria-label*="More options"]',
'button[aria-label*="More"]',
'button[aria-label*="更多"]',
'button:has(mat-icon[data-mat-icon-name="more_vert"])',
'button:has(mat-icon[fonticon="more_vert"])',
'button:has(mat-icon)'
].join(', '),
deleteItem: [
'button[data-test-id="delete-button"]',
'[role="menu"] button:has-text("删除")',
'[role="menu"] button:has-text("Delete")',
'.mat-mdc-menu-panel button:has-text("删除")',
'.mat-mdc-menu-panel button:has-text("Delete")',
'div[role="menu"] button:has(mat-icon[data-mat-icon-name="delete"])',
'div[role="menu"] button:has(mat-icon[fonticon="delete"])',
'div[role="menu"] [role="menuitem"]:has(mat-icon[data-mat-icon-name="delete"])',
'div[role="menu"] [role="menuitem"]:has(mat-icon[fonticon="delete"])',
'button:has(mat-icon[data-mat-icon-name="delete"])',
'button:has(mat-icon[fonticon="delete"])'
].join(', '),
confirmButton: [
'mat-dialog-container gem-button[data-test-id="confirm-button"]',
'.cdk-overlay-pane gem-button[data-test-id="confirm-button"]',
'gem-button[data-test-id="confirm-button"]',
'mat-dialog-container gem-button[data-test-id="confirm-button"] button',
'.cdk-overlay-pane gem-button[data-test-id="confirm-button"] button',
'gem-button[data-test-id="confirm-button"] button',
'mat-dialog-container button[data-test-id="confirm-button"]',
'mat-dialog-container button:has-text("Delete")',
'mat-dialog-container button:has-text("删除")',
'.cdk-overlay-pane button:has-text("Delete")',
'.cdk-overlay-pane button:has-text("删除")',
'button[data-test-id="confirm-button"]'
].join(', ')
};
const log = (...args) => console.log(LOG_PREFIX, ...args);
const warn = (...args) => console.warn(LOG_PREFIX, ...args);
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
const getPreferredLanguage = () => {
const languages = [
document.documentElement.lang,
...Array.from(navigator.languages || []),
navigator.language
];
const hasChinese = languages.some(language => String(language || '').toLowerCase().startsWith('zh'));
if (hasChinese) return 'zh';
const hasEnglish = languages.some(language => String(language || '').toLowerCase().startsWith('en'));
if (hasEnglish) return 'en';
return 'zh';
};
const CURRENT_LANGUAGE = getPreferredLanguage();
const formatMessage = (key, params = {}) => {
const message = (MESSAGES[CURRENT_LANGUAGE] || MESSAGES.zh)[key] || MESSAGES.zh[key];
return typeof message === 'function' ? message(params) : message;
};
const getText = (node) => (node && node.textContent ? node.textContent.trim() : '');
const clickElement = (node) => {
const nodeWindow = node.ownerDocument.defaultView || window;
const mouseEventInit = { bubbles: true, cancelable: true, view: nodeWindow };
node.dispatchEvent(new nodeWindow.MouseEvent('pointerdown', mouseEventInit));
node.dispatchEvent(new nodeWindow.MouseEvent('mousedown', mouseEventInit));
node.dispatchEvent(new nodeWindow.MouseEvent('pointerup', mouseEventInit));
node.dispatchEvent(new nodeWindow.MouseEvent('mouseup', mouseEventInit));
node.click();
};
const getConversationIdFromHref = (href) => {
if (!href) return '';
const match = href.match(/\/c\/([a-f0-9-]{36})/i);
return match ? match[1] : '';
};
const queryTextSelector = (root, selector) => {
const hasTextMatch = selector.match(/^(.*):has-text\("(.+)"\)$/);
if (!hasTextMatch) return root.querySelector(selector);
const [, baseSelector, expectedText] = hasTextMatch;
return Array.from(root.querySelectorAll(baseSelector))
.find(node => getText(node).includes(expectedText)) || null;
};
const queryFirst = (selector, root = document) => {
const selectors = selector.split(',').map(item => item.trim()).filter(Boolean);
for (const item of selectors) {
try {
const node = queryTextSelector(root, item);
if (node) return node;
} catch (error) {
warn('Selector failed', item, error);
}
}
return null;
};
const waitForElement = async (selector, root = document, timeoutMs = 1500) => {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const node = queryFirst(selector, root);
if (node) return node;
await delay(100);
}
throw new Error(`Element not found: ${selector}`);
};
const isVisible = (node) => {
if (!node || !node.isConnected) return false;
const rect = node.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const waitForNodeRemoval = async (node, timeoutMs = 8000) => {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
if (!node || !node.isConnected) return;
await delay(150);
}
throw new Error('Conversation node was not removed after delete confirmation');
};
const sendRequest = (details) => {
if (typeof GM_xmlhttpRequest === 'function') {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
...details,
onload: resolve,
onerror: reject
});
});
}
return fetch(details.url, {
method: details.method,
headers: details.headers,
body: details.data,
credentials: 'include'
}).then(async response => ({
status: response.status,
responseText: await response.text()
}));
};
const addStyle = (css) => {
if (typeof GM_addStyle === 'function') {
GM_addStyle(css);
return;
}
const style = document.createElement('style');
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
};
class BaseAdapter {
getConversations() { throw new Error('Not implemented'); }
injectCheckbox(node, callback) { throw new Error('Not implemented'); }
deleteConversation(node) { throw new Error('Not implemented'); }
archiveConversation(node) { return this.deleteConversation(node); }
getSidebarHeader() { throw new Error('Not implemented'); }
getNodeId(node) { return getText(node); }
findConversationById(id) {
return this.getConversations().find(node => this.getNodeId(node) === id) || null;
}
}
class OpenAIAdapter extends BaseAdapter {
constructor() {
super();
this.accessToken = '';
this.fetchAccessToken();
}
fetchAccessToken() {
return sendRequest({
method: 'GET',
url: 'https://chatgpt.com/api/auth/session',
}).then((res) => {
try {
const data = JSON.parse(res.responseText);
this.accessToken = data.accessToken || '';
log('ChatGPT token loaded', Boolean(this.accessToken));
} catch(e) {
console.error(LOG_PREFIX, 'Failed to parse ChatGPT token', e);
}
}).catch((error) => {
console.error(LOG_PREFIX, 'Failed to load ChatGPT token', error);
});
}
getConversations() {
const links = Array.from(document.querySelectorAll(`nav ${CONVERSATION_LINK_SELECTOR}`))
.filter(link => getConversationIdFromHref(link.getAttribute('href')));
const seen = new Set();
return links.filter(link => {
const id = getConversationIdFromHref(link.getAttribute('href'));
if (seen.has(id)) return false;
seen.add(id);
return true;
});
}
getNodeId(node) {
return getConversationIdFromHref(node.getAttribute('href'));
}
injectCheckbox(node, onSelectChange) {
if (node.querySelector('.bulk-delete-checkbox')) return;
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'bulk-delete-checkbox';
checkbox.addEventListener('click', (event) => event.stopPropagation());
checkbox.addEventListener('change', (e) => onSelectChange(node, e.target.checked, e));
node.insertBefore(checkbox, node.firstChild);
}
async deleteConversation(node) {
const href = node.getAttribute('href');
const id = getConversationIdFromHref(href);
if (!id) return Promise.reject('No conversation ID found');
if (!this.accessToken) {
await this.fetchAccessToken();
}
if (!this.accessToken) return Promise.reject('No Access Token');
const res = await sendRequest({
method: 'PATCH',
url: `https://chatgpt.com/backend-api/conversation/${id}`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`
},
data: JSON.stringify({ is_visible: false })
});
if (res.status >= 200 && res.status < 300) {
node.remove();
log('ChatGPT hidden', id, res.status);
return;
}
throw new Error(`HTTP error ${res.status}`);
}
async archiveConversation(node) {
const href = node.getAttribute('href');
const id = getConversationIdFromHref(href);
if (!id) return Promise.reject('No conversation ID found');
if (!this.accessToken) {
await this.fetchAccessToken();
}
if (!this.accessToken) return Promise.reject('No Access Token');
const res = await sendRequest({
method: 'PATCH',
url: `https://chatgpt.com/backend-api/conversation/${id}`,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${this.accessToken}`
},
data: JSON.stringify({ is_archived: true })
});
if (res.status >= 200 && res.status < 300) {
node.remove();
log('ChatGPT archived', id, res.status);
return;
}
throw new Error(`HTTP error ${res.status}`);
}
getSidebarHeader() {
const firstConversation = this.getConversations()[0];
return firstConversation?.closest('ul') || document.querySelector('nav');
}
}
class GoogleAdapter extends BaseAdapter {
getConversations() {
const nodes = Array.from(document.querySelectorAll(GEMINI_SELECTORS.conversation))
.map(node => node.closest('gem-nav-list-item[data-test-id="conversation"]') || node)
.filter(node => !node.closest('#bulk-controls-panel'));
return Array.from(new Set(nodes));
}
getNodeId(node) {
const href = node.querySelector('a[href^="/app/"]')?.getAttribute('href');
return href || getText(node);
}
injectCheckbox(node, onSelectChange) {
if (node.querySelector('.bulk-delete-checkbox')) return;
node.classList.add('gemini-bulk-delete-row');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'bulk-delete-checkbox';
checkbox.addEventListener('click', (event) => event.stopPropagation());
checkbox.addEventListener('change', (e) => onSelectChange(node, e.target.checked, e));
const anchor = node.querySelector('a.mat-mdc-list-item') || node.firstChild;
node.insertBefore(checkbox, anchor);
}
async deleteConversation(node) {
const nodeId = this.getNodeId(node);
node.scrollIntoView({ block: 'center' });
node.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }));
const menuBtn = await waitForElement(GEMINI_SELECTORS.menuButton, node, 1500);
clickElement(menuBtn);
const deleteItem = await waitForElement(GEMINI_SELECTORS.deleteItem, document, 1500);
clickElement(deleteItem);
const confirmBtn = await this.waitForConfirmButton();
clickElement(confirmBtn);
await this.waitForDeletedNode(node, nodeId);
log('Gemini delete clicked', getText(node).slice(0, 80));
}
isConversationDeleted(node, nodeId) {
return !node || !node.isConnected || Boolean(nodeId && !this.findConversationById(nodeId));
}
async waitForDeletedNode(node, nodeId) {
const startedAt = Date.now();
while (Date.now() - startedAt < 10000) {
const confirmBtn = await this.findConfirmButton();
const isDeleted = this.isConversationDeleted(node, nodeId);
if (isDeleted && confirmBtn) {
const cancelBtn = await this.findCancelButton();
if (cancelBtn) {
clickElement(cancelBtn);
await delay(GEMINI_DELETE_POLL_MS);
if (!await this.findConfirmButton()) return;
}
}
if (isDeleted && !confirmBtn) return;
if (confirmBtn) {
clickElement(confirmBtn);
}
await delay(GEMINI_DELETE_POLL_MS);
}
if (!this.isConversationDeleted(node, nodeId)) {
await waitForNodeRemoval(node, 1);
}
}
async findConfirmButton() {
const confirmHosts = Array.from(document.querySelectorAll([
'mat-dialog-container gem-button[data-test-id="confirm-button"]',
'.cdk-overlay-pane gem-button[data-test-id="confirm-button"]'
].join(', '))).filter(isVisible);
if (confirmHosts.length > 0) {
const host = confirmHosts[confirmHosts.length - 1];
return host.querySelector('button') || host;
}
const buttons = Array.from(document.querySelectorAll([
'mat-dialog-container gem-button[data-test-id="confirm-button"] button',
'.cdk-overlay-pane gem-button[data-test-id="confirm-button"] button',
'mat-dialog-container button',
'.cdk-overlay-pane button',
'button[data-test-id="confirm-button"]'
].join(', '))).filter(isVisible);
const confirmButtons = buttons.filter(button => {
const text = getText(button);
return text === '删除'
|| text === 'Delete'
|| button.matches('[data-test-id="confirm-button"]')
|| Boolean(button.closest('gem-button[data-test-id="confirm-button"]'));
});
return confirmButtons.length > 0 ? confirmButtons[confirmButtons.length - 1] : null;
}
async findCancelButton() {
const cancelHosts = Array.from(document.querySelectorAll([
'mat-dialog-container gem-button[data-test-id="cancel-button"]',
'.cdk-overlay-pane gem-button[data-test-id="cancel-button"]'
].join(', '))).filter(isVisible);
if (cancelHosts.length > 0) {
const host = cancelHosts[cancelHosts.length - 1];
return host.querySelector('button') || host;
}
const buttons = Array.from(document.querySelectorAll([
'mat-dialog-container button',
'.cdk-overlay-pane button'
].join(', '))).filter(isVisible);
return buttons.find(button => ['取消', 'Cancel'].includes(getText(button))) || null;
}
async waitForConfirmButton() {
const startedAt = Date.now();
while (Date.now() - startedAt < 3000) {
const confirmButton = await this.findConfirmButton();
if (confirmButton) {
return confirmButton;
}
await delay(100);
}
throw new Error(`Element not found: ${GEMINI_SELECTORS.confirmButton}`);
}
getSidebarHeader() {
return queryFirst(GEMINI_SELECTORS.historyRoot) || document.querySelector('nav');
}
}
const getAdapter = () => {
const host = window.location.hostname;
if (host.includes('chatgpt.com')) {
return new OpenAIAdapter();
} else if (host.includes('gemini.google.com')) {
return new GoogleAdapter();
}
return null;
};
class BulkManager {
constructor() {
this.adapter = getAdapter();
if (!this.adapter) return;
this.selectedNodes = new Set();
this.isDeleting = false;
this.init();
}
init() {
addStyle(`
.bulk-delete-checkbox { width: 16px; height: 16px; margin: 4px 8px 4px 0; cursor: pointer; flex: 0 0 auto; }
#bulk-controls-panel { padding: 8px; display: flex; align-items: center; gap: 8px; border-bottom: 1px solid rgba(0,0,0,0.16); background: rgba(255,255,255,0.92); color: #111; position: sticky; top: 0; z-index: 9999; font-size: 12px; }
#bulk-controls-panel.bulk-controls-panel-gemini { margin: 4px 0 8px; border: 1px solid rgba(255,255,255,0.16); border-radius: 8px; background: rgba(32,32,32,0.96); color: #f2f2f2; box-sizing: border-box; max-width: 100%; }
#bulk-controls-panel.bulk-controls-panel-chatgpt { margin: 4px 0 8px; border-radius: 8px; box-sizing: border-box; max-width: 100%; }
.bulk-btn { padding: 4px 8px; cursor: pointer; border-radius: 4px; border: 1px solid #aaa; background: #fff; color: #111; }
.bulk-btn:disabled { cursor: not-allowed; opacity: 0.5; }
.bulk-status { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.gemini-bulk-delete-row { display: flex !important; align-items: center !important; }
`);
this.startObserver();
this.injectPanel();
}
injectPanel() {
if (document.getElementById('bulk-controls-panel')) return;
const isGemini = window.location.hostname.includes('gemini.google.com');
const isChatGPT = window.location.hostname.includes('chatgpt.com');
const header = isGemini ? document.querySelector('.chat-history-list') : this.adapter.getSidebarHeader();
if (!header) return;
const panel = document.createElement('div');
panel.id = 'bulk-controls-panel';
if (isGemini) {
panel.classList.add('bulk-controls-panel-gemini');
} else if (isChatGPT) {
panel.classList.add('bulk-controls-panel-chatgpt');
}
const selectAll = document.createElement('input');
selectAll.type = 'checkbox';
selectAll.id = 'bulk-select-all';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'bulk-btn';
deleteBtn.id = 'bulk-delete-btn';
deleteBtn.textContent = `${formatMessage('deleteSelected')} (0)`;
const archiveBtn = document.createElement('button');
archiveBtn.className = 'bulk-btn';
archiveBtn.id = 'bulk-archive-btn';
archiveBtn.textContent = `${formatMessage('archiveSelected')} (0)`;
const stopBtn = document.createElement('button');
stopBtn.className = 'bulk-btn';
stopBtn.id = 'bulk-stop-btn';
stopBtn.textContent = formatMessage('stop');
stopBtn.disabled = true;
const status = document.createElement('span');
status.className = 'bulk-status';
status.id = 'bulk-status';
status.textContent = formatMessage('ready');
if (isChatGPT) {
panel.append(selectAll, archiveBtn, deleteBtn, stopBtn, status);
} else {
panel.append(selectAll, deleteBtn, stopBtn, status);
}
if (isGemini) {
header.parentElement.insertBefore(panel, header);
} else if (isChatGPT && header.parentElement) {
header.parentElement.insertBefore(panel, header);
} else {
header.insertBefore(panel, header.firstChild);
}
selectAll.addEventListener('change', (e) => {
const list = this.adapter.getConversations();
list.forEach(node => {
const cb = node.querySelector('.bulk-delete-checkbox');
if (cb) {
cb.checked = e.target.checked;
this.onSelectChange(node, e.target.checked);
}
});
});
archiveBtn.addEventListener('click', () => this.runBulkArchive());
deleteBtn.addEventListener('click', () => this.runBulkDelete());
stopBtn.addEventListener('click', () => {
this.stopRequested = true;
this.setStatus(formatMessage('stopping'));
});
}
onSelectChange(node, isSelected) {
if (isSelected) {
this.selectedNodes.add(node);
} else {
this.selectedNodes.delete(node);
}
this.refreshSelectedState();
}
refreshSelectedState() {
const btn = document.getElementById('bulk-delete-btn');
const archiveBtn = document.getElementById('bulk-archive-btn');
if (btn) {
btn.innerText = `${formatMessage('deleteSelected')} (${this.selectedNodes.size})`;
}
if (archiveBtn) {
archiveBtn.innerText = `${formatMessage('archiveSelected')} (${this.selectedNodes.size})`;
}
}
syncSelectedFromDom() {
this.selectedNodes.clear();
this.adapter.getConversations().forEach(node => {
const checkbox = node.querySelector('.bulk-delete-checkbox');
if (checkbox?.checked) {
this.selectedNodes.add(node);
}
});
this.refreshSelectedState();
}
setStatus(message) {
const status = document.getElementById('bulk-status');
if (status) status.innerText = message;
}
scanAndInject() {
const list = this.adapter.getConversations();
list.forEach(node => this.adapter.injectCheckbox(node, (n, s, e) => this.onSelectChange(n, s, e)));
this.injectPanel();
this.setStatus(formatMessage('detectedConversations', { count: list.length }));
}
startObserver() {
this.scanAndInject();
this.observer = new MutationObserver(() => {
window.clearTimeout(this.scanTimer);
this.scanTimer = window.setTimeout(() => this.scanAndInject(), 150);
});
this.observer.observe(document.body, { childList: true, subtree: true });
}
async runBulkDelete() {
this.syncSelectedFromDom();
const actionLabel = formatMessage('deleteAction');
if (this.selectedNodes.size === 0) {
this.setStatus(formatMessage('noSelection'));
return;
}
if (!window.confirm(formatMessage('confirmAction', { actionLabel, count: this.selectedNodes.size }))) {
this.setStatus(formatMessage('cancelled'));
return;
}
await this.runBulkAction('delete');
}
async runBulkArchive() {
await this.runBulkAction('archive');
}
async runBulkAction(action) {
this.syncSelectedFromDom();
if (this.isDeleting || this.selectedNodes.size === 0) {
this.setStatus(formatMessage('noSelection'));
return;
}
this.isDeleting = true;
this.stopRequested = false;
const btn = document.getElementById('bulk-delete-btn');
const archiveBtn = document.getElementById('bulk-archive-btn');
const stopBtn = document.getElementById('bulk-stop-btn');
const nodes = Array.from(this.selectedNodes)
.map(node => ({ node, id: this.adapter.getNodeId(node) }));
if (btn) btn.disabled = true;
if (archiveBtn) archiveBtn.disabled = true;
if (stopBtn) stopBtn.disabled = false;
const actionLabel = action === 'archive' ? formatMessage('archiveAction') : formatMessage('deleteAction');
for (let i = 0; i < nodes.length; i++) {
if (this.stopRequested) break;
const entry = nodes[i];
const node = entry.node.isConnected ? entry.node : this.adapter.findConversationById(entry.id);
if (!node) {
this.selectedNodes.delete(entry.node);
continue;
}
const activeBtn = action === 'archive' ? archiveBtn : btn;
if (activeBtn) {
activeBtn.innerText = formatMessage('runningAction', {
actionLabel,
current: i + 1,
total: nodes.length
});
}
this.setStatus(formatMessage('processing', { current: i + 1, total: nodes.length }));
try {
if (action === 'archive') {
await this.adapter.archiveConversation(node);
} else {
await this.adapter.deleteConversation(node);
}
this.selectedNodes.delete(node);
} catch(e) {
console.error(LOG_PREFIX, `${actionLabel} failed`, e);
this.setStatus(formatMessage('failed', { message: e.message || e }));
}
await delay(BULK_DELETE_INTERVAL_MS);
}
this.refreshSelectedState();
const selectAll = document.getElementById('bulk-select-all');
if (selectAll) selectAll.checked = false;
if (btn) btn.disabled = false;
if (archiveBtn) archiveBtn.disabled = false;
if (stopBtn) stopBtn.disabled = true;
this.setStatus(this.stopRequested ? formatMessage('stopped') : formatMessage('done'));
this.isDeleting = false;
}
}
const boot = () => {
if (window.__aiChatBulkManager) return;
if (!document.body || !document.documentElement) {
window.setTimeout(boot, 100);
return;
}
window.__aiChatBulkManager = new BulkManager();
log('initialized');
};
if (window.location.hostname.includes('gemini.google.com')) {
window.setTimeout(boot, GEMINI_BOOT_DELAY_MS);
} else {
boot();
}
})();