Greasy Fork is available in English.

ChatGPT Bulk Deleter

Adds a "Select Chats" button to ChatGPT for deleting multiple conversations at once. Bypasses the UI and uses direct API calls for speed and reliability.

// ==UserScript==
// @name         ChatGPT Bulk Deleter
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Adds a "Select Chats" button to ChatGPT for deleting multiple conversations at once. Bypasses the UI and uses direct API calls for speed and reliability.
// @author       Tano
// @match        https://chatgpt.com/*
// @connect      chatgpt.com
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Styles for the UI controls ---
    GM_addStyle(`
        .bulk-delete-controls { padding: 8px; margin: 8px; border: 1px solid var(--token-border-light); border-radius: 12px; background-color: var(--token-main-surface-primary); display: flex; flex-direction: column; gap: 8px; }
        .bulk-delete-btn { display: inline-block; width: 100%; padding: 10px 12px; border: none; border-radius: 8px; cursor: pointer; text-align: center; font-size: 14px; font-weight: 500; transition: background-color 0.2s, color 0.2s; }
        #toggle-select-btn { background-color: #4C50D3; color: white; }
        #toggle-select-btn:hover { background-color: #3a3eab; }
        #toggle-select-btn.selection-active { background-color: #FFD6D6; color: #D34C4C; }
        #delete-selected-btn { background-color: #D34C4C; color: white; display: none; }
        #delete-selected-btn:hover { background-color: #b03a3a; }
        #delete-selected-btn:disabled { background-color: #7c7c7c; cursor: not-allowed; }
        .chat-selectable { cursor: cell !important; }
        a.chat-selected { background-color: rgba(76, 80, 211, 0.2) !important; border: 1px solid #4C50D3 !important; border-radius: 8px; }
    `);

    let selectionMode = false;
    const selectedChats = new Set();
    let authToken = null;

    /**
     * Fetches the authorization token required for API calls.
     * The token is cached after the first successful retrieval.
     */
    async function getAuthToken() {
        if (authToken) return authToken;
        try {
            const response = await new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "GET",
                    url: "https://chatgpt.com/api/auth/session",
                    onload: resolve,
                    onerror: reject
                });
            });
            const data = JSON.parse(response.responseText);
            if (data && data.accessToken) {
                authToken = data.accessToken;
                console.log("Auth Token successfully retrieved.");
                return authToken;
            }
            throw new Error("accessToken not found in session response.");
        } catch (error) {
            console.error("Failed to retrieve auth token:", error);
            alert("Could not retrieve authorization token. The script cannot continue.");
            return null;
        }
    }

    /**
     * Creates and injects the control buttons into the UI.
     */
    function initialize() {
        const historyContainer = document.querySelector('#history');
        if (!historyContainer || document.getElementById('toggle-select-btn')) return;

        getAuthToken(); // Pre-fetch the token on load

        console.log("ChatGPT Bulk Deleter: Initializing Command Center...");
        const controlsContainer = document.createElement('div');
        controlsContainer.className = 'bulk-delete-controls';
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'toggle-select-btn';
        toggleBtn.className = 'bulk-delete-btn';
        toggleBtn.textContent = 'Select Chats to Delete';
        toggleBtn.onclick = toggleSelectionMode;
        const deleteBtn = document.createElement('button');
        deleteBtn.id = 'delete-selected-btn';
        deleteBtn.className = 'bulk-delete-btn';
        deleteBtn.textContent = 'Delete Selected (0)';
        deleteBtn.onclick = deleteSelectedChats;
        controlsContainer.appendChild(toggleBtn);
        controlsContainer.appendChild(deleteBtn);
        historyContainer.parentNode.insertBefore(controlsContainer, historyContainer);
    }

    /**
     * Toggles the chat selection mode on/off.
     */
    function toggleSelectionMode() {
        selectionMode = !selectionMode;
        const toggleBtn = document.getElementById('toggle-select-btn'), deleteBtn = document.getElementById('delete-selected-btn'), chatItems = document.querySelectorAll('a[href^="/c/"]');
        if (selectionMode) {
            toggleBtn.textContent = 'Cancel Selection';
            toggleBtn.classList.add('selection-active');
            deleteBtn.style.display = 'block';
            chatItems.forEach(chat => { chat.classList.add('chat-selectable'); chat.addEventListener('click', handleChatClick, true); });
        } else {
            toggleBtn.textContent = 'Select Chats to Delete';
            toggleBtn.classList.remove('selection-active');
            deleteBtn.style.display = 'none';
            chatItems.forEach(chat => { chat.classList.remove('chat-selectable', 'chat-selected'); chat.removeEventListener('click', handleChatClick, true); });
            selectedChats.clear();
            updateDeleteButton();
        }
    }

    /**
     * Handles clicks on chat items when in selection mode.
     */
    function handleChatClick(event) {
        event.preventDefault();
        event.stopPropagation();
        const chatElement = event.currentTarget;
        if (selectedChats.has(chatElement)) {
            selectedChats.delete(chatElement);
            chatElement.classList.remove('chat-selected');
        } else {
            selectedChats.add(chatElement);
            chatElement.classList.add('chat-selected');
        }
        updateDeleteButton();
    }

    /**
     * Updates the counter on the delete button.
     */
    function updateDeleteButton() {
        const deleteBtn = document.getElementById('delete-selected-btn');
        if(deleteBtn) deleteBtn.textContent = `Delete Selected (${selectedChats.size})`;
    }

    /**
     * Main function to delete selected chats via API calls.
     */
    async function deleteSelectedChats() {
        if (selectedChats.size === 0) return alert('Please select at least one chat first.');
        const token = await getAuthToken();
        if (!token) return;
        if (!confirm(`Are you sure you want to delete ${selectedChats.size} chat(s)? This action is irreversible.`)) return;

        const chatsToDelete = Array.from(selectedChats);
        const total = chatsToDelete.length;
        const deleteBtn = document.getElementById('delete-selected-btn');
        const toggleBtn = document.getElementById('toggle-select-btn');
        deleteBtn.disabled = true;
        toggleBtn.disabled = true;

        let successCount = 0;
        let errorCount = 0;

        for (let i = 0; i < total; i++) {
            const chatElement = chatsToDelete[i];
            const href = chatElement.getAttribute('href');
            const conversationId = href.split('/').pop();
            const chatTitle = chatElement.textContent.trim();

            deleteBtn.textContent = `Deleting... (${i + 1}/${total})`;
            console.log(`[API CALL] Deleting chat ${i + 1}/${total}: "${chatTitle}" (ID: ${conversationId})`);

            try {
                // The API call that "hides" the chat, effectively deleting it from view.
                await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: "PATCH",
                        url: `https://chatgpt.com/backend-api/conversation/${conversationId}`,
                        headers: {
                            "Content-Type": "application/json",
                            "Authorization": `Bearer ${token}`
                        },
                        data: JSON.stringify({ is_visible: false }),
                        onload: (response) => {
                            if (response.status >= 200 && response.status < 300) {
                                resolve(response);
                            } else {
                                reject(new Error(`Server responded with status ${response.status}`));
                            }
                        },
                        onerror: reject
                    });
                });
                console.log(`  -> [SUCCESS] Chat "${chatTitle}" successfully hidden via API.`);
                chatElement.style.transition = 'opacity 0.5s';
                chatElement.style.opacity = '0';
                setTimeout(() => chatElement.remove(), 500); // Visually remove from the list
                successCount++;

            } catch (error) {
                console.error(`  -> [FAIL] API call failed for "${chatTitle}":`, error);
                chatElement.style.border = '2px solid red'; // Mark chats that failed to delete
                errorCount++;
            }
        }

        alert(`Complete. Successfully deleted: ${successCount}. Errors: ${errorCount}.`);
        deleteBtn.disabled = false;
        toggleBtn.disabled = false;
        toggleSelectionMode(); // Reset the UI
    }

    // --- Observer to initialize the script when the history list is loaded ---
    const observer = new MutationObserver(() => {
        if (document.querySelector('#history') && !document.getElementById('toggle-select-btn')) {
            initialize();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();