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.

// ==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 });
})();