ChatGPT AutoCleaner v5

Bugfix & speed-up for ChatGPT: cleans the conversation chat window by trimming old messages from the Browser DOM. Keeps only the latest N turns visible, preventing lag and excessive DOM size on long sessions. Includes manual “Clean now” button and auto-clean toggle.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ChatGPT AutoCleaner v5
// @version      1.5
// @description  Bugfix & speed-up for ChatGPT: cleans the conversation chat window by trimming old messages from the Browser DOM. Keeps only the latest N turns visible, preventing lag and excessive DOM size on long sessions. Includes manual “Clean now” button and auto-clean toggle.
// @author       Aleksey Maximov <[email protected]>
// @match        https://chat.openai.com/*
// @match        https://chatgpt.com/*
// @grant        none
// @namespace    81e29c9d-b6e3-4210-b862-c93cb160f09a
// @license      MIT
// ==/UserScript==



/*
WHY THIS FIX EXISTS (read this once):

Problem:
- The ChatGPT web app keeps adding conversation turns to the DOM indefinitely.
- On long sessions the DOM grows huge → reflows/repaints become expensive → UI lags.
- React also keeps its own internal arrays with messages, drafts, telemetry and other
  data that never gets freed properly. This is a major source of memory bloat, but
  the safe way to clean it without breaking features is still unclear.

What this script changes:
- On a timer, it trims old DOM turns (visual cleanup only).
- Adds a **Clean now** button to trigger immediate trim.
- Auto-clean can be enabled/disabled, and interval/keep count adjusted.
- Skips auto-clean when the tab is hidden.

Result:
- Only the latest N messages remain in the DOM.
- Old nodes are removed → browser memory/paint workload drops.
- React’s hidden memory leaks still remain, but DOM cleanup alone already makes
  long sessions much smoother. Further fixes are TBD.
*/


(function () {
  'use strict';

  // ---------- UI ----------
  function injectUI() {
    if (document.getElementById("chatgpt-cleaner-panel")) return;

    const defaults = { leaveOnly: 5, intervalSec: 10, enabled: false };
    const stored = {
      leaveOnly: parseInt(localStorage.getItem("chatgpt-leaveOnly")) || defaults.leaveOnly,
      intervalSec: parseInt(localStorage.getItem("chatgpt-intervalSec")) || defaults.intervalSec,
      enabled: localStorage.getItem("chatgpt-enabled") !== "false"
    };

    const container = document.createElement("div");
    container.id = "chatgpt-cleaner-wrapper";
    Object.assign(container.style, {
      position: "fixed", bottom: "8px", right: "8px", zIndex: 9999, fontFamily: "sans-serif"
    });

    const toggleButton = document.createElement("button");
    toggleButton.textContent = "⚙";
    Object.assign(toggleButton.style, {
      background: stored.enabled ? "#444" : "red", color: "#fff", border: "none",
      borderRadius: "4px", padding: "2px 6px", cursor: "pointer", fontSize: "14px"
    });
    toggleButton.title = "Toggle cleaner panel";

    const panel = document.createElement("div");
    panel.id = "chatgpt-cleaner-panel";
    Object.assign(panel.style, {
      display: "none", marginTop: "4px", background: "#222", color: "#fff",
      padding: "10px 12px 10px 10px", borderRadius: "6px", fontSize: "12px",
      boxShadow: "0 0 6px rgba(0,0,0,0.5)", border: "1px solid #555", position: "relative", opacity: "0.95"
    });

    panel.innerHTML = `
      <div id="chatgpt-close" style="position:absolute;top:0px;right:2px;font-size:16px;font-weight:bold;color:#ccc;cursor:pointer;">✖</div>
      <label>
        Keep <input id="chatgpt-keep-count" type="number" value="${stored.leaveOnly}" min="1"
        style="width:52px;min-width:52px;padding:2px 6px 2px 4px;font-size:12px;background:#111;color:#fff;border:1px solid #555;box-sizing:border-box;"> messages
      </label>
      <br>
      <label>
        Interval <input id="chatgpt-interval" type="number" value="${stored.intervalSec}" min="2"
        style="width:52px;min-width:52px;padding:2px 6px 2px 4px;font-size:12px;background:#111;color:#fff;border:1px solid #555;box-sizing:border-box;"> sec
      </label>
      <br>
      <label><input type="checkbox" id="chatgpt-enabled" ${stored.enabled ? "checked" : ""}> Auto-clean enabled</label>
      <br>
      <button id="chatgpt-clean-now" style="
        margin-top:6px;background:#008000;color:#fff;border:none;border-radius:4px;
        padding:2px 8px;cursor:pointer;font-size:12px;">Clean now</button>
    `;

    toggleButton.onclick = () => { panel.style.display = "block"; toggleButton.style.display = "none"; };
    container.appendChild(toggleButton);
    container.appendChild(panel);
    document.body.appendChild(container);

    const countInput = panel.querySelector("#chatgpt-keep-count");
    const intervalInput = panel.querySelector("#chatgpt-interval");
    const enabledCheckbox = panel.querySelector("#chatgpt-enabled");
    const cleanNowBtn = panel.querySelector("#chatgpt-clean-now");
    const closeBtn = panel.querySelector("#chatgpt-close");

    let leaveOnly = stored.leaveOnly;
    let intervalMs = Math.max(2000, stored.intervalSec * 1000);
    let enabled = stored.enabled;
    let intervalId = null;


    function scheduleClean(force = false) {
      if (!force) {
        if (!enabled) return;
        if (document.hidden) return;
      }
      cleanOldMessages(force);
    }


    // ---------- main cleaner (no gating here; gating is in scheduleClean) ----------
    function cleanOldMessages(manual = false) {
      try {
        if (manual) console.info("[AutoCleaner] Manual clean");
        // 1) Trim DOM (visual only)
        const all = document.querySelectorAll('[data-testid^="conversation-turn-"]');
        if (all.length) {
          const lastAttr = all[all.length - 1].getAttribute("data-testid");
          const last = parseInt(lastAttr?.split("-")[2]);
          if (!isNaN(last)) {
            all.forEach(item => {
              const idx = parseInt(item.getAttribute("data-testid")?.split("-")[2]);
              if (!isNaN(idx) && idx < last - leaveOnly) item.remove();
            });
          }
        }
        // console.info("[AutoCleaner] Working...");

      } catch (e) {
        console.error("[AutoCleaner] clean error:", e);
      }
    }

    function startCleaner() {
      if (intervalId) clearInterval(intervalId);
      intervalId = setInterval(() => scheduleClean(false), intervalMs);
      console.info(`[AutoCleaner] Started: interval=${intervalMs}ms, keep=${leaveOnly}`);
    }

    // ---------- handlers ----------
    enabledCheckbox.onchange = () => {
      enabled = enabledCheckbox.checked;
      localStorage.setItem("chatgpt-enabled", enabled);
      toggleButton.style.background = enabled ? "#444" : "red";
      console.debug("[AutoCleaner] enabled =", enabled);
    };

    countInput.oninput = () => {
      const val = parseInt(countInput.value);
      if (!isNaN(val) && val > 0) {
        leaveOnly = val;
        localStorage.setItem("chatgpt-leaveOnly", val);
        console.debug("[AutoCleaner] keep set to", leaveOnly);
      }
    };

    intervalInput.oninput = () => {
      const val = parseInt(intervalInput.value);
      if (!isNaN(val) && val > 1) {
        intervalMs = Math.max(2000, val * 1000);
        localStorage.setItem("chatgpt-intervalSec", val);
        startCleaner();
      }
    };

    cleanNowBtn.onclick = () => {
      console.info("[AutoCleaner] CLEAN NOW clicked");
      scheduleClean(true);
      panel.style.display = "none";
      toggleButton.style.display = "inline-block";
    };

    closeBtn.onclick = () => {
      panel.style.display = "none";
      toggleButton.style.display = "inline-block";
    };

    startCleaner();
  }

  if (document.readyState === "complete" || document.readyState === "interactive") {
    injectUI();
  } else {
    window.addEventListener("DOMContentLoaded", injectUI);
  }

  const observer = new MutationObserver(() => {
    if (!document.getElementById("chatgpt-cleaner-wrapper")) injectUI();
  });
  observer.observe(document.body, { childList: true, subtree: true });
})();