ChatGPT.com Advanced Message Culler

Culls messages from ChatGPT conversations via an overlay. Helpful for those who have low-end hardwares

As of 11/04/2025. See the latest version.

// ==UserScript==
// @name         ChatGPT.com Advanced Message Culler
// @namespace    https://chatgpt.com/
// @version      1.3.6
// @description  Culls messages from ChatGPT conversations via an overlay. Helpful for those who have low-end hardwares
// @author       sytesn, ChatGPT
// @match        https://chatgpt.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let MESSAGE_LIMIT = 50;
    let cullingEnabled = false;
    let mode = "hide";
    const revealBatch = 20;

    // Load settings from localStorage
    const storedEnabled = localStorage.getItem('culler-enabled');
    const storedLimit = localStorage.getItem('culler-limit');
    const storedMode = localStorage.getItem('culler-mode');

    cullingEnabled = storedEnabled === "true";
    MESSAGE_LIMIT = parseInt(storedLimit, 10) || MESSAGE_LIMIT;
    mode = storedMode || mode;

    const style = document.createElement('style');
    style.textContent = `
        #culler-panel {
            position: fixed;
            top: 10px;
            left: 10px;
            z-index: 9999;
            background-color: #111;
            padding: 10px;
            border-radius: 8px;
            font-family: Arial, sans-serif;
            font-size: 13px;
            color: #fff;
        }
        #culler-panel input, #culler-panel select {
            background-color: #222;
            color: #fff;
            border: 1px solid #444;
            border-radius: 4px;
            padding: 2px 4px;
        }
        #culler-panel button {
            margin-top:6px;
            padding:4px 8px;
            border: none;
            border-radius: 4px;
            background-color: #444;
            color: #fff;
            cursor: pointer;
        }
        #showMoreOverlay {
            position: sticky;
            top: 0;
            background: rgba(50,50,50,0.95);
            color: #fff;
            padding: 10px;
            text-align: center;
            z-index: 10000;
            border-radius: 0 0 8px 8px;
            box-shadow: 0 4px 10px rgba(0,0,0,0.4);
            user-select: none;
        }
    `;
    document.head.appendChild(style);

    const panel = document.createElement('div');
    panel.id = 'culler-panel';
    panel.innerHTML = `
        <div style="margin-bottom:6px;font-weight:bold;">Message Culler</div>
        <label style="display:block; margin-bottom:4px;">
            <input type="checkbox" id="culler-enable">
            Enable
        </label>
        <label style="display:block;margin-top:6px;">
            Keep last <input type="number" id="culler-limit" value="${MESSAGE_LIMIT}" style="width: 50px;">
        </label>
        <label style="display:block;margin-top:6px;">
            Mode:
            <select id="culler-mode">
                <option value="hide">Hide</option>
                <option value="remove">Remove</option>
            </select>
        </label>
        <button id="culler-refresh">Refresh Now</button>
    `;
    document.body.appendChild(panel);

    (function makeDraggable(el) {
        // Load previous position
        const savedLeft = localStorage.getItem('culler-pos-left');
        const savedTop = localStorage.getItem('culler-pos-top');
        if (savedLeft && savedTop) {
            el.style.left = savedLeft;
            el.style.top = savedTop;
        }

        let isDragging = false, offsetX, offsetY;
        el.addEventListener('mousedown', e => {
            if (["INPUT", "SELECT", "BUTTON"].includes(e.target.tagName)) return;
            isDragging = true;
            offsetX = e.clientX - el.offsetLeft;
            offsetY = e.clientY - el.offsetTop;
        });
        document.addEventListener('mousemove', e => {
            if (isDragging) {
                const left = (e.clientX - offsetX);
                const top = (e.clientY - offsetY);
                el.style.left = `${left}px`;
                el.style.top = `${top}px`;
                localStorage.setItem('culler-pos-left', `${left}px`);
                localStorage.setItem('culler-pos-top', `${top}px`);
            }
        });
        document.addEventListener('mouseup', () => isDragging = false);
    })(panel);

    const enableCheckbox = document.getElementById('culler-enable');
    const limitInput = document.getElementById('culler-limit');
    const modeSelect = document.getElementById('culler-mode');
    const refreshBtn = document.getElementById('culler-refresh');

    // Apply stored values to inputs
    enableCheckbox.checked = cullingEnabled;
    limitInput.value = MESSAGE_LIMIT;
    modeSelect.value = mode;

    enableCheckbox.addEventListener('change', () => {
        cullingEnabled = enableCheckbox.checked;
        localStorage.setItem('culler-enabled', cullingEnabled);
        applyCulling();
    });

    limitInput.addEventListener('change', () => {
        MESSAGE_LIMIT = parseInt(limitInput.value, 10) || MESSAGE_LIMIT;
        localStorage.setItem('culler-limit', MESSAGE_LIMIT);
        applyCulling();
    });

    modeSelect.addEventListener('change', () => {
        mode = modeSelect.value;
        localStorage.setItem('culler-mode', mode);
        applyCulling();
    });

    refreshBtn.addEventListener('click', () => {
        applyCulling();
    });

    function getMessageElements() {
        return Array.from(document.querySelectorAll('article[data-testid^="conversation-turn-"]'));
    }

    function applyCulling() {
        const messages = getMessageElements();
        const total = messages.length;
        if (!cullingEnabled) {
            messages.forEach(msg => {
                msg.style.display = '';
                msg.classList.remove('culled');
            });
            return;
        }
        messages.forEach((msg, i) => {
            if (i < total - MESSAGE_LIMIT) {
                if (mode === "remove") {
                    if (!msg.dataset.removed) {
                        msg.dataset.removed = "true";
                        msg.remove();
                    }
                } else if (mode === "hide") {
                    msg.style.display = "none";
                    msg.classList.add("culled");
                }
            } else {
                msg.style.display = "";
                msg.classList.remove("culled");
            }
        });
    }

    let showMoreOverlay = null;
    function createShowMoreOverlay() {
        if (showMoreOverlay || mode !== "hide") return;

        const convoContainer = document.querySelector("div.mt-1\\.5.flex.flex-col.text-sm.md\\:pb-9");
        if (!convoContainer) return;

        showMoreOverlay = document.createElement("div");
        showMoreOverlay.id = "showMoreOverlay";
        updateShowMoreText();

        showMoreOverlay.addEventListener("click", () => {
            doRevealBatch();
            if (document.querySelectorAll('article.culled').length === 0) {
                removeShowMoreOverlay();
            }
        });

        convoContainer.prepend(showMoreOverlay);
    }

    function updateShowMoreText() {
        if (showMoreOverlay) {
            const hiddenCount = document.querySelectorAll('article.culled').length;
            showMoreOverlay.textContent = `Show ${revealBatch} older messages (${hiddenCount} hidden)`;
        }
    }

    function removeShowMoreOverlay() {
        if (showMoreOverlay) {
            showMoreOverlay.remove();
            showMoreOverlay = null;
        }
    }

    function doRevealBatch() {
        const culled = Array.from(document.querySelectorAll('article.culled'));
        let count = 0;
        culled.slice(0, revealBatch).forEach(msg => {
            msg.style.display = "";
            msg.classList.remove("culled");
            count++;
        });
        updateShowMoreText();
    }

    const scrollContainer = document.querySelector('main') || document.body;
    scrollContainer.addEventListener("scroll", () => {
        if (scrollContainer.scrollTop === 0 && mode === "hide" && document.querySelectorAll('article.culled').length > 0) {
            createShowMoreOverlay();
        } else {
            removeShowMoreOverlay();
        }
    });

    window.addEventListener("resize", () => {
        if (scrollContainer.scrollTop === 0 && mode === "hide" && document.querySelectorAll('article.culled').length > 0) {
            createShowMoreOverlay();
        } else {
            removeShowMoreOverlay();
        }
    });

    setInterval(() => {
        if (scrollContainer.scrollTop === 0 && mode === "hide" && document.querySelectorAll('article.culled').length > 0) {
            createShowMoreOverlay();
            updateShowMoreText();
        } else {
            removeShowMoreOverlay();
        }
    }, 1500);

    const observer = new MutationObserver(() => {
        if (cullingEnabled) {
            applyCulling();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    setTimeout(() => {
        applyCulling();
    }, 1500);
})();