ChatGPT Auto Cleaner — v2.6 (navigation synced with KEEP_LAST)

Навигация и элементы управления остаются только у последних сообщений (связано с KEEP_LAST)

// ==UserScript==
// @name         ChatGPT Auto Cleaner — v2.6 (navigation synced with KEEP_LAST)
// @namespace    https://presinfo.com/
// @version      2.6
// @author       Vladyslav Shyshlov
// @description  Навигация и элементы управления остаются только у последних сообщений (связано с KEEP_LAST)
// @match        https://chatgpt.com/*
// @run-at       document-idle
// @grant        none
// @license MIT
// ==/UserScript==

(function () {
  'use strict';

  const KEEP_LAST      = 30;       // сколько сообщений хранить                         how many messages to keep
  const BUFFER_TURNS   = 2;        // небольшой запас, чтобы не удалить поток           small buffer to avoid deleting an active stream
  const INTERVAL       = 90000;    // каждые 20 секунд это 20000 сейчас 90000 это 1.30 минуты       Every 20 seconds — right now it’s 90000, which is 1.30 minutes
  const FIRST_DELAY    = 3000;     // первая мягкая очистка                             first soft cleanup
  const IDLE_MS        = 5000;     // тайм-аут покоя                                    idle timeout

  let lastActivity = performance.now();

  const poke = () => { lastActivity = performance.now(); };
  window.addEventListener('keydown', poke, true);
  window.addEventListener('pointerdown', poke, true);
  window.addEventListener('input', poke, true);

  const moStream = new MutationObserver(poke);
  moStream.observe(document.documentElement, {subtree: true, childList: true, attributes: true});

  const safeForEach = (nodes, fn) =>
    Array.from(nodes).forEach(el => { try { if (el && el.isConnected) fn(el); } catch {} });

  const isCalm = () => {
    const now = performance.now();
    if (now - lastActivity < IDLE_MS) return false;
    if (document.querySelector('form textarea:focus')) return false;
    if (document.querySelector('.result-streaming,[data-writing-block]')) return false;
    return true;
  };

  function removeOldMessages() {
    const main = document.querySelector('main');
    if (!main) return 0;

    const messages = main.querySelectorAll(
      'article[data-testid^="conversation-turn"], article[data-turn], div[data-message-author-role]'
    );
    const limit = KEEP_LAST + BUFFER_TURNS;
    if (messages.length <= limit) return 0;

    // старые сообщения
    const toRemove = Array.from(messages).slice(0, messages.length - limit);

    safeForEach(toRemove, el => {
      // перед удалением — удаляем навигацию, связанную с этим блоком
      const nav = el.querySelectorAll(
        'div.z-0.flex.min-h-\\[46px\\].justify-start,' +
        'div[aria-label*="actions"],' +
        'div[data-testid*="turn-actions"],' +
        'div[class*="pointer-events-none"][class*="opacity-0"][class*="group-hover/turn-messages"]'
      );
      safeForEach(nav, n => n.remove());

      // теперь сам элемент
      el.remove();
    });

    return toRemove.length;
  }

  function removeEmptyBlocks() {
    // белые квадраты
    safeForEach(document.querySelectorAll('article[data-turn]'), a => {
      const hasText  = a.textContent?.trim().length > 0;
      const hasMedia = a.querySelector('img, picture, video');
      const hasInput = a.querySelector('textarea, input, [contenteditable="true"]');
      if (!hasText && !hasMedia && !hasInput) a.remove();
    });

    // пустые контейнеры
    safeForEach(document.querySelectorAll('.flex.max-w-full.flex-col.grow, .text-base.my-auto.mx-auto'), div => {
      const hasText  = div.textContent?.trim().length > 0;
      const hasMedia = div.querySelector('img, video, picture');
      if (!hasText && !hasMedia) div.remove();
    });

    // зависшие навигации, если остались вне article
    const allNavs = document.querySelectorAll(
      '#thread div.z-0.flex.min-h-\\[46px\\].justify-start,' +
      'div[aria-label*="actions"],' +
      'div[data-testid*="turn-actions"],' +
      'div[class*="pointer-events-none"][class*="opacity-0"][class*="group-hover/turn-messages"]'
    );

    // сохраняем только последние KEEP_LAST (синхронно с сообщениями)
    if (allNavs.length > KEEP_LAST) {
      const toRemove = Array.from(allNavs).slice(0, allNavs.length - KEEP_LAST);
      safeForEach(toRemove, n => n.remove());
    }
  }

  const schedule = (fn) => {
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => setTimeout(fn, 0), { timeout: 1200 });
    } else {
      setTimeout(fn, 0);
    }
  };

  function clean() {
    if (!isCalm()) return;
    schedule(() => {
      const removed = removeOldMessages();
      removeEmptyBlocks();
      if (removed > 0)
        console.log(`🧹 Удалено ${removed} сообщений и связанных панелей, оставлено ${KEEP_LAST}`);
    });
  }

  console.log(`✅ ChatGPT Auto Cleaner v2.6 запущен (навигация синхронизирована с KEEP_LAST = ${KEEP_LAST})`);
  setTimeout(clean, FIRST_DELAY);
  setInterval(clean, INTERVAL);
})();