Raw Source
sukhoi191 / ChatGPT cleanup filters

// ==UserScript==
// @name         ChatGPT cleanup filters
// @namespace    https://github.com/sukhoi191/chatgpt-cleanup
// @version      1.0.0
// @author       sukhoi191
// @license      MIT
// @updateURL    https://raw.githubusercontent.com/sukhoi191/chatgpt-cleanup/refs/heads/main/chatgpt-cleanup-filters.meta.js
// @downloadURL  https://raw.githubusercontent.com/sukhoi191/chatgpt-cleanup/refs/heads/main/chatgpt-cleanup-filters.user.js
// @description  Hide unwanted ChatGPT UI with configurable filter groups.
// @match        https://chatgpt.com/*
// @match        https://*.chatgpt.com/*
// @run-at       document-start
// @grant        none
// @noframes
// ==/UserScript==

// MIT License
// 
// Copyright (c) 2026 sukhoi191
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom
// the Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
// A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
// OR OTHER DEALINGS IN THE SOFTWARE.

// Generated by node build.mjs. Edit the source files, then rebuild.
(() => {
'use strict';
// Set a group to false to disable all its rules, then save and reload ChatGPT.
// followUps includes desktop/mobile suggestions and their spacing.
// reactions includes emoji and their reserved spacing.
// Duplicate/malformed follow-up fixes have independent switches.
const FILTER_CONFIG = {
  "reactions": true,
  "memoryDiveDeeper": true,
  "writingBlockFollowUps": true,
  "followUps": true,
  "duplicateFollowUps": true,
  "malformedFollowUps": true,
  "feedbackPrompts": true,
  "upsellBanners": true
};
const FILTER_RULES = [
  {
    "group": "reactions",
    "selector": "div[aria-live=\"polite\"][aria-atomic=\"true\"]:has(> div[role=\"img\"][aria-label^=\"ChatGPT reacted with \"])"
  },
  {
    "group": "memoryDiveDeeper",
    "selector": "section[data-memory-section-id=\"dive-deeper\"]"
  },
  {
    "group": "writingBlockFollowUps",
    "selector": "button[class~=\"group/suggested-followup\"]",
    "upward": 3
  },
  {
    "group": "reactions",
    "selector": "div:has(> div[aria-live=\"polite\"][aria-atomic=\"true\"] > div[role=\"img\"][aria-label^=\"ChatGPT reacted with \"]) + div:has(.turn-action-controls)",
    "style": "margin-inline-end: 0 !important"
  },
  {
    "group": "followUps",
    "selector": "span[role=\"button\"]",
    "xpath": "ancestor::ul[1][.//span[contains(concat(' ', normalize-space(@class), ' '), ' group/followup-citation ')] or (@data-is-only-node and @data-is-last-node and .//span[contains(concat(' ', normalize-space(@class), ' '), ' entity-underline ')])]/preceding-sibling::*[1][self::p]/preceding-sibling::*[1]",
    "style": "margin-bottom:0 !important"
  },
  {
    "group": "followUps",
    "selector": "span[role=\"button\"][class~=\"entity-underline\"]",
    "xpath": "ancestor::ul[1][@data-is-only-node and @data-is-last-node] | ancestor::ul[1][@data-is-only-node and @data-is-last-node]/preceding-sibling::*[1][self::p]"
  },
  {
    "group": "followUps",
    "selector": "span[role=\"button\"][class~=\"group/followup-citation\"]",
    "xpath": "ancestor::ul[1] | ancestor::ul[1]/preceding-sibling::*[1][self::p]"
  },
  {
    "group": "duplicateFollowUps",
    "selector": "span[role=\"button\"]:is([class~=\"group/followup-citation\"],[class~=\"entity-underline\"])",
    "xpath": "ancestor::ul[1]/preceding-sibling::*[2][self::ul][count(li) = count(following-sibling::*[2][self::ul]/li) and count(li[. = ancestor::ul[1]/following-sibling::*[2][self::ul]/li]) = count(li)] | ancestor::ul[1]/preceding-sibling::*[2][self::ul][count(li) = count(following-sibling::*[2][self::ul]/li) and count(li[. = ancestor::ul[1]/following-sibling::*[2][self::ul]/li]) = count(li)]/preceding-sibling::*[1][self::p]"
  },
  {
    "group": "duplicateFollowUps",
    "selector": "span[role=\"button\"]:is([class~=\"group/followup-citation\"],[class~=\"entity-underline\"])",
    "xpath": "ancestor::ul[1]/preceding-sibling::*[2][self::ul][count(li) = count(following-sibling::*[2][self::ul]/li) and count(li[. = ancestor::ul[1]/following-sibling::*[2][self::ul]/li]) = count(li)]/preceding-sibling::*[1][self::p]/preceding-sibling::*[1]",
    "style": "margin-bottom:0 !important"
  },
  {
    "group": "malformedFollowUps",
    "selector": "span[role=\"button\"]:is([class~=\"group/followup-citation\"],[class~=\"entity-underline\"])",
    "xpath": "ancestor::ul[1][not(following-sibling::*)]/preceding-sibling::*[2][self::ul][not(.//span[@role=\"button\"])] | ancestor::ul[1][not(following-sibling::*)]/preceding-sibling::*[2][self::ul][not(.//span[@role=\"button\"])]/preceding-sibling::*[1][self::p]"
  },
  {
    "group": "malformedFollowUps",
    "selector": "span[role=\"button\"]:is([class~=\"group/followup-citation\"],[class~=\"entity-underline\"])",
    "xpath": "ancestor::ul[1][not(following-sibling::*)]/preceding-sibling::*[2][self::ul][not(.//span[@role=\"button\"])]/preceding-sibling::*[1][self::p]/preceding-sibling::*[1]",
    "style": "margin-bottom:0 !important"
  },
  {
    "group": "feedbackPrompts",
    "selector": "aside:has(button[aria-pressed] + button[aria-pressed])",
    "upward": 1
  },
  {
    "group": "upsellBanners",
    "selector": "aside[class^=\"surface-\"]:has(div[class^=\"dismiss-\"])",
    "upward": 1
  }
];
// CSS filters remain live automatically; procedural filters are reconciled
// after DOM changes. Hide nodes instead of removing React-owned elements.
const rules = FILTER_RULES.filter(rule => FILTER_CONFIG[rule.group] !== false).map(rule => ({
    ...rule,
    style: rule.style ?? 'display: none !important;',
    steps: rule.upward !== undefined ? [{ type: 'upward', count: rule.upward }]
        : rule.xpath !== undefined ? [{ type: 'xpath', expression: rule.xpath }] : [],
}));
const attribute = 'data-vm-chatgpt-cleanup';
const procedural = [];
const cssRules = [];
for (const [index, rule] of rules.entries()) {
    if (!rule.steps.length) {
        cssRules.push(`${rule.selector} { ${rule.style} }`);
    } else {
        const id = `r${index}`;
        procedural.push({ ...rule, id });
        cssRules.push(`[${attribute}~="${id}"] { ${rule.style} }`);
    }
}
const styleElement = document.createElement('style');
styleElement.id = 'vm-chatgpt-cleanup-filters';
styleElement.textContent = cssRules.join('\n');
let marked = new Map();
const failedRules = new Set();

function evaluate(rule) {
    let elements = new Set(document.querySelectorAll(rule.selector));
    for (const step of rule.steps) {
        const next = new Set();
        for (const element of elements) {
            if (step.type === 'upward') {
                let parent = element;
                for (let i = 0; i < step.count && parent; i++) parent = parent.parentElement;
                if (parent) next.add(parent);
            } else {
                const result = document.evaluate(step.expression, element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
                for (let i = 0; i < result.snapshotLength; i++) {
                    const node = result.snapshotItem(i);
                    if (node.nodeType === Node.ELEMENT_NODE) next.add(node);
                }
            }
        }
        elements = next;
    }
    return elements;
}

function reconcile() {
    if (!styleElement.isConnected && document.documentElement) {
        (document.head || document.documentElement).appendChild(styleElement);
    }
    const next = new Map();
    for (const rule of procedural) {
        if (failedRules.has(rule.id)) continue;
        try {
            for (const element of evaluate(rule)) {
                if (!next.has(element)) next.set(element, []);
                next.get(element).push(rule.id);
            }
        } catch (error) {
            failedRules.add(rule.id);
            console.error('[ChatGPT cleanup] Invalid filter:', rule.selector, error);
        }
    }
    for (const element of marked.keys()) {
        if (!next.has(element)) element.removeAttribute(attribute);
    }
    for (const [element, ids] of next) {
        const value = ids.join(' ');
        if (element.getAttribute(attribute) !== value) element.setAttribute(attribute, value);
    }
    marked = next;
}

let scheduled = false;
function schedule() {
    if (scheduled) return;
    scheduled = true;
    // Batch streaming updates and keep running in background tabs.
    setTimeout(() => {
        scheduled = false;
        reconcile();
    }, 50);
}

const observer = new MutationObserver(records => {
    // Our own markers must not trigger another full DOM scan.
    if (records.some(record => record.type !== 'attributes' || record.attributeName !== attribute)) schedule();
});
observer.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
reconcile();

})();