GPT Optimum

Improves ChatGPT performance by virtualizing off-screen messages.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

作者のサイトでサポートを受ける。または、このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         GPT Optimum
// @namespace    https://github.com/YashRana738/GptOptimum
// @version      2.2
// @description  Improves ChatGPT performance by virtualizing off-screen messages.
// @author       Yash Rana
// @license      MIT
// @homepageURL  https://github.com/YashRana738/GptOptimum
// @supportURL   https://github.com/YashRana738/GptOptimum/issues
// @match        *://chatgpt.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    // ── Helpers ──────────────────────────────────────────────────
    const $ = (s, root = document) => root.querySelector(s);
    const $$ = (s, root = document) => root.querySelectorAll(s);
    const isChatRoute = () => location.pathname.startsWith('/c/');
    const getChatId = () => {
        const m = location.pathname.match(/^\/c\/([a-f0-9-]+)/);
        return m ? m[1] : null;
    };

    // ── Settings (GM storage) ───────────────────────────────────
    const DEFAULTS = { enabled: true, debug: false, aggressive: false, instantNewChat: true, topCenter: false, uiAlpha: 0.5, alwaysExpand: false };

    const cfg = {
        get(k) { return GM_getValue(k, DEFAULTS[k]); },
        set(k, v) { GM_setValue(k, v); },
        all() { return { enabled: this.get('enabled'), debug: this.get('debug'), aggressive: this.get('aggressive'), instantNewChat: this.get('instantNewChat'), topCenter: this.get('topCenter'), uiAlpha: this.get('uiAlpha'), alwaysExpand: this.get('alwaysExpand') }; },
    };

    // ── Early UI lock (document-start) ──────────────────────────
    if (cfg.get('enabled') && isChatRoute()) {
        const bootId = getChatId();
        const storedBootId = cfg.get('lastChatId');
        const isNewChat = bootId && storedBootId && storedBootId < bootId && cfg.get('instantNewChat');
        if (!isNewChat) {
            document.documentElement.classList.add('cgv-loading');
        }
        if (bootId && (!storedBootId || bootId > storedBootId)) cfg.set('lastChatId', bootId);
    }

    // ── CSS ─────────────────────────────────────────────────────
    const CSS_VARS = `
    :root{--cgv-bg:#fff;--cgv-text:#0d0d0d;--cgv-muted:#666;--cgv-border:#e5e5e5;--cgv-shadow:0 4px 6px -1px rgb(0 0 0/.1);--cgv-primary:#10a37f;--cgv-ui-alpha:0.5}
    html.dark,:root{--cgv-bg:#fff;--cgv-text:#0d0d0d;--cgv-muted:#666;--cgv-border:#e5e5e5}
    html.dark{--cgv-bg:#2f2f2f;--cgv-text:#ececec;--cgv-muted:#b4b4b4;--cgv-border:#424242;--cgv-shadow:0 4px 6px -1px rgb(0 0 0/.5)}
    @media(prefers-color-scheme:dark){html:not(.light){--cgv-bg:#2f2f2f;--cgv-text:#ececec;--cgv-muted:#b4b4b4;--cgv-border:#424242;--cgv-shadow:0 4px 6px -1px rgb(0 0 0/.5)}}
    `;

    const GLOBAL_CSS = `
    ${CSS_VARS}
    html.cgv-loading body { pointer-events: none !important; user-select: none !important; cursor: wait !important; opacity: 0.7 !important; transition: opacity 0.3s; }
    .cgv-off { content-visibility: hidden !important; }
    body.cgv-debug .cgv-off { background: rgba(255,0,0,.1) !important; border: 1px dashed red !important; position: relative; }
    body.cgv-debug .cgv-off::after { content: "Unloaded"; position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); color: red; font-weight: 700; font-size: 14px; pointer-events: none; }
    `;

    const SHADOW_CSS = `
    ${CSS_VARS}
    .cgv-wrap { position: fixed; bottom: 24px; right: 24px; z-index: 999999; display: flex; flex-direction: column; align-items: flex-end; pointer-events: auto; }
    .cgv-pill { display: flex; align-items: center; background: var(--cgv-bg); color: var(--cgv-text); border: 1px solid var(--cgv-border); box-shadow: var(--cgv-shadow); padding: 8px 14px; border-radius: 20px; font: 500 13px/1 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; cursor: pointer; white-space: nowrap; box-sizing: border-box; opacity: var(--cgv-ui-alpha); transform: translateY(10px); gap: 8px; max-width: 220px; transition: opacity .25s ease, transform .4s cubic-bezier(.34,1.56,.64,1), max-width .4s cubic-bezier(.4,0,.2,1), padding .4s cubic-bezier(.4,0,.2,1), height .4s cubic-bezier(.4,0,.2,1), border-radius .4s cubic-bezier(.4,0,.2,1), border-color .25s, gap .4s cubic-bezier(.4,0,.2,1); height: 34px; overflow: hidden; }
    .cgv-pill.on { opacity: var(--cgv-ui-alpha); transform: translateY(0); }
    .cgv-pill.on:hover, .cgv-pill.busy, .cgv-pill.dead { opacity: 1 !important; }
    .cgv-pill.mini { max-width: 30px; padding: 10px; border-radius: 50%; gap: 0; height: 30px; }
    .cgv-pill.mini .cgv-dot { margin: 0; }
    .cgv-wrap:hover .cgv-pill.mini { max-width: 220px; padding: 8px 14px; border-radius: 20px; height: 34px; gap: 8px; }
    .cgv-wrap:hover .cgv-pill.mini .cgv-label { max-width: 150px; }
    .cgv-dot { width: 8px; height: 8px; min-width: 8px; min-height: 8px; border-radius: 50%; flex-shrink: 0; position: relative; z-index: 2; background: var(--cgv-primary); box-shadow: 0 0 6px var(--cgv-primary); animation: cgv-pulse 2s infinite; transition: background .25s, box-shadow .25s, border .25s; }
    .cgv-label { display: inline-block; overflow: hidden; max-width: 150px; transition: max-width .4s cubic-bezier(.4,0,.2,1); }
    .cgv-pill.mini .cgv-label { max-width: 0; }
    .cgv-pill.busy { border-color: var(--cgv-primary); opacity: 1 !important; }
    .cgv-pill.busy .cgv-dot { animation: cgv-spin .8s linear infinite; background: 0 0; border: 2px solid var(--cgv-primary); border-top-color: transparent; width: 10px; height: 10px; box-shadow: none; }
    .cgv-pill.dead { border-color: #ef4444; }
    .cgv-pill.dead .cgv-dot { background: #ef4444; box-shadow: 0 0 6px #ef4444; animation: none; }
    .cgv-block { position: fixed; bottom: 24px; right: 24px; background: #ef4444; color: #fff; padding: 10px 18px; border-radius: 25px; font: 600 13px -apple-system,sans-serif; box-shadow: 0 10px 25px -5px rgba(239,68,68,.4); z-index: 1000000; display: flex; align-items: center; gap: 8px; transform: translateY(120px); opacity: 0; transition: transform .6s cubic-bezier(.68,-.6,.32,1.6), opacity .35s; }
    .cgv-block.on { transform: translateY(0); opacity: 1; }
    .cgv-block svg { width: 16px; height: 16px; stroke: currentColor; stroke-width: 2.5; fill: none; }
    .cgv-pill.pushed { transform: translateY(-64px) !important; }
    .cgv-panel { position: absolute; bottom: calc(100% + 14px); right: 0; width: 290px; background: var(--cgv-bg); color: var(--cgv-text); border: 1px solid var(--cgv-border); border-radius: 18px; box-shadow: 0 8px 28px rgba(0,0,0,.16); font-family: 'Inter',-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; opacity: 0; transform: translateY(6px) scale(.97); pointer-events: none; transition: opacity .18s, transform .22s cubic-bezier(.34,1.56,.64,1); z-index: 1000001; overflow: visible; }
    .cgv-panel::after { content: ""; position: absolute; top: 100%; left: 0; right: 0; height: 22px; }
    .cgv-wrap:hover .cgv-panel { opacity: 1; transform: translateY(0) scale(1); pointer-events: auto; }
    .cgv-top-pos { top: 24px; bottom: auto; left: 50%; right: auto; transform: translateX(-50%); align-items: center; }
    .cgv-top-pos .cgv-pill.pushed { transform: translateY(0) !important; }
    .cgv-top-pos .cgv-panel { top: calc(100% + 14px); bottom: auto; right: auto; left: 50%; transform: translate(-50%, -6px) scale(.97); }
    .cgv-top-pos .cgv-panel::after { bottom: 100%; top: auto; height: 22px; }
    .cgv-top-pos:hover .cgv-panel { transform: translate(-50%, 0) scale(1); }
    .cgv-block.cgv-top-pos { top: 74px; bottom: auto; left: 50%; right: auto; transform: translate(-50%, -120px); }
    .cgv-block.cgv-top-pos.on { transform: translate(-50%, 0); }
    .cgv-ph { padding: 18px 16px 8px; text-align: center; }
    .cgv-ph h3 { margin: 0; font-size: 15px; font-weight: 600; display: flex; align-items: center; justify-content: center; gap: 8px; }
    .cgv-ph p { margin: 3px 0 0; font-size: 11px; color: var(--cgv-muted); }
    .cgv-ph .cgv-hd { width: 9px; height: 9px; background: var(--cgv-primary); border-radius: 50%; box-shadow: 0 0 6px var(--cgv-primary); flex-shrink: 0; }
    .cgv-pc { padding: 8px 16px 16px; }
    .cgv-loading-msg { display: none; padding: 28px 16px; text-align: center; font-size: 13px; font-weight: 500; color: var(--cgv-muted); }
    .cgv-loading-msg .cgv-lspin { display: inline-block; width: 18px; height: 18px; border: 2px solid var(--cgv-border); border-top-color: var(--cgv-primary); border-radius: 50%; animation: cgv-spin .7s linear infinite; margin-bottom: 10px; }
    .cgv-panel.loading .cgv-pc { display: none; }
    .cgv-panel.loading .cgv-loading-msg { display: block; }
    .cgv-card { background: var(--cgv-bg); border: 1px solid var(--cgv-border); border-radius: 14px; padding: 12px 14px; margin-bottom: 8px; display: flex; align-items: center; justify-content: space-between; box-shadow: 0 1px 3px rgba(0,0,0,.05); cursor: pointer; transition: border-color .2s, transform .15s; }
    .cgv-card:last-child { margin-bottom: 0; }
    .cgv-card:hover { border-color: var(--cgv-primary); transform: translateY(-1px); }
    .cgv-card.off { opacity: .45; pointer-events: none; filter: grayscale(.5); }
    .cgv-card.main { border-color: var(--cgv-primary); margin-bottom: 14px; }
    .cgv-ci { display: flex; flex-direction: column; gap: 1px; }
    .cgv-cl { font-size: 13px; font-weight: 500; }
    .cgv-cd { font-size: 10.5px; color: var(--cgv-muted); }
    .cgv-sw { position: relative; display: inline-block; width: 34px; height: 19px; flex-shrink: 0; }
    .cgv-sw input { opacity: 0; width: 0; height: 0; }
    .cgv-sl { position: absolute; cursor: pointer; inset: 0; background: var(--cgv-border); border-radius: 19px; transition: .25s cubic-bezier(.4, 0, .2, 1); }
    .cgv-sl::before { content: ""; position: absolute; width: 13px; height: 13px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; box-shadow: 0 1px 3px rgba(0,0,0,.2); transition: .25s cubic-bezier(.4, 0, .2, 1); }
    .cgv-sw input:checked + .cgv-sl { background: var(--cgv-primary); }
    .cgv-sw input:checked + .cgv-sl::before { transform: translateX(15px); }
    .cgv-range { width: 100%; height: 4px; background: var(--cgv-border); border-radius: 2px; appearance: none; outline: none; margin-top: 8px; }
    .cgv-range::-webkit-slider-thumb { appearance: none; width: 12px; height: 12px; background: var(--cgv-primary); border-radius: 50%; cursor: pointer; transition: transform .1s; }
    .cgv-range::-webkit-slider-thumb:hover { transform: scale(1.2); }
    .cgv-ft { padding: 0 16px 12px; text-align: center; font-size: 10px; color: var(--cgv-muted); opacity: .5; }
    @keyframes cgv-spin { to { transform: rotate(360deg); } }
    @keyframes cgv-pulse { 0%, 100% { opacity: .5; } 50% { opacity: 1; } }
    `;

    GM_addStyle(GLOBAL_CSS);

    // ── Virtualizer ─────────────────────────────────────────────
    class Virtualizer {
        constructor() {
            this.enabled = false;
            this.aggressive = false;
            this.io = null;
        }

        boot(enabled, aggressive) {
            this.enabled = enabled;
            this.aggressive = aggressive;
            if (enabled) this._createObserver();
        }

        _margin() { return this.aggressive ? '5% 0px' : '100% 0px'; }

        _createObserver() {
            this.io?.disconnect();
            this.io = new IntersectionObserver(entries => {
                if (!this.enabled) return;
                for (const e of entries) {
                    if (e.isIntersecting) this._restore(e.target);
                    else this._unload(e.target);
                }
            }, { rootMargin: this._margin(), threshold: 0 });
            $$('.cgv-msg').forEach(el => this.io.observe(el));
        }

        setEnabled(v) {
            this.enabled = v;
            if (v) this._createObserver();
            else { this.io?.disconnect(); $$('.cgv-off').forEach(el => this._restore(el)); }
        }

        setAggressive(v) {
            const changed = this.aggressive !== v;
            this.aggressive = v;
            if (changed && this.enabled) this._createObserver();
        }

        setDebug(v) {
            document.body?.classList.toggle('cgv-debug', v);
        }

        observe(el) { this.io?.observe(el); }

        _unload(el) {
            if (el.classList.contains('cgv-off')) return;
            const h = Math.round(el.getBoundingClientRect().height);
            if (h < 60) return;
            el.style.containIntrinsicSize = `auto ${h}px`;
            el.classList.add('cgv-off');
        }
        _restore(el) {
            if (!el.classList.contains('cgv-off')) return;
            el.classList.remove('cgv-off');
            el.style.containIntrinsicSize = '';
        }
    }

    const virt = new Virtualizer();

    // ── Selectors ───────────────────────────────────────────────
    const MSG_SEL = '[data-message-author-role],article,[data-testid^="conversation-turn-"],.w-full.text-token-text-primary,.group.w-full.border-b';

    // ── App ─────────────────────────────────────────────────────
    class App {
        constructor() {
            this.tracked = new Set();
            this.pill = null;
            this.wrap = null;
            this.blocker = null;
            this.panel = null;
            this.busy = isChatRoute();
            this.lastUrl = location.href;
            this.collapseTimer = null;
            this.statusLock = false;
            this.shadow = null;
            this.host = null;
        }

        async run() {
            await this._waitBody();
            const s = cfg.all();

            virt.boot(s.enabled, s.aggressive);
            virt.setDebug(s.debug);

            this._buildUI(s);
            this._observe();
            this._watchUrl();
            this._guard(); // Persistence guard

            if (s.enabled && this.busy) this._optimise();
            else { this.busy = false; this._unlock(); this._sync(s.enabled); }
        }

        // ── Input blocker during optimisation ──────────────────────
        _blockInputs() {
            const handler = e => {
                if (!this.busy || !virt.enabled) return;
                // Never block events inside our UI wrapper
                if (this.wrap?.contains(e.target)) return;
                e.stopImmediatePropagation();
                e.preventDefault();
            };
            for (const t of ['keydown', 'keyup', 'keypress', 'contextmenu', 'mousedown', 'click', 'dblclick', 'auxclick'])
                window.addEventListener(t, handler, true);
        }

        // ── URL change detection (SPA) ────────────────────────────
        _watchUrl() {
            setInterval(() => {
                if (location.href === this.lastUrl) return;
                const prevUrl = this.lastUrl;
                this.lastUrl = location.href;
                // Was the previous page the homepage (not a /c/ chat route)?
                const cameFromHome = !prevUrl.includes('/c/');

                if (isChatRoute() && virt.enabled) {
                    const newId = getChatId();

                    // ── Instant optimise new chats ──
                    if (newId) {
                        const storedId = cfg.get('lastChatId');
                        if (!storedId || newId > storedId) {
                            cfg.set('lastChatId', newId);
                        }
                        if (cfg.get('instantNewChat') && cameFromHome && storedId && storedId < newId) {
                            this.tracked.clear();
                            this.busy = false;
                            this._unlock();
                            this.pill?.classList.remove('pushed');
                            this.blocker?.classList.remove('on');
                            this.panel?.classList.remove('loading');
                            const lbl = $('.cgv-label', this.pill);
                            if (lbl) lbl.textContent = 'New chat detected';
                            this.statusLock = true;
                            this.pill?.classList.remove('busy', 'dead', 'mini');
                            this.pill?.classList.add('on');
                            this._scheduleCollapse(3000);
                            this._observeNewChat();
                            return;
                        }
                    }

                    this.tracked.clear();
                    this.busy = true;
                    document.documentElement.classList.add('cgv-loading');
                    this._showBusy();
                    setTimeout(() => { if (this.busy) { this.pill?.classList.add('pushed'); this.blocker?.classList.add('on'); } }, 400);
                    this._optimise(true);
                } else {
                    this.busy = false;
                    this._unlock();
                    this._sync(virt.enabled);
                    this.pill?.classList.remove('pushed');
                    this.blocker?.classList.remove('on');
                }
            }, 500);
        }

        // ── Wait for <body> ───────────────────────────────────────
        _waitBody() {
            return new Promise(r => {
                if (document.body) return r();
                new MutationObserver((_, o) => { if (document.body) { o.disconnect(); r(); } })
                    .observe(document.documentElement, { childList: true });
            });
        }

        // ── Build all UI elements (using Shadow Root) ──────────────
        _buildUI(s) {
            if (this.host && document.contains(this.host)) return;
            
            this.host = document.createElement('div');
            this.host.id = 'cgv-host';
            this.shadow = this.host.attachShadow({ mode: 'closed' });

            const style = document.createElement('style');
            style.textContent = SHADOW_CSS;
            this.shadow.appendChild(style);

            // Wrapper
            this.wrap = document.createElement('div');
            this.wrap.className = 'cgv-wrap';

            // Pill
            this.pill = document.createElement('div');
            this.pill.className = 'cgv-pill';
            this.pill.innerHTML = '<div class="cgv-dot"></div><span class="cgv-label">Optimised</span>';
            this.pill.addEventListener('click', () => {
                if (this.busy) return;
                this.pill.classList.toggle('mini');
                if (this.pill.classList.contains('mini')) { clearTimeout(this.collapseTimer); }
                else this._scheduleCollapse();
            });

            // Blocker
            this.blocker = document.createElement('div');
            this.blocker.className = 'cgv-block';
            this.blocker.innerHTML = '<svg viewBox="0 0 24 24"><path d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg><span>Input temporarily blocked</span>';

            // Panel
            this.panel = this._buildPanel(s);

            // Initial state
            if (s.topCenter) {
                this.wrap.classList.add('cgv-top-pos');
                this.blocker.classList.add('cgv-top-pos');
            }

            if (this.busy && s.enabled) {
                this._showBusy();
                setTimeout(() => { if (this.busy) { this.pill?.classList.add('pushed'); this.blocker?.classList.add('on'); } }, 400);
            }

            this.wrap.append(this.panel, this.pill);
            this.shadow.append(this.wrap, this.blocker);
            document.body.appendChild(this.host);
            this._blockInputs();
        }

        // ── Build settings panel ──────────────────────────────────
        _buildPanel(s) {
            const el = document.createElement('div');
            el.className = 'cgv-panel' + (this.busy ? ' loading' : '');
            el.innerHTML = `
        <div class="cgv-ph"><h3><div class="cgv-hd"></div>GPT Optimum</h3><p>Optimize ChatGPT Performance</p></div>
        <div class="cgv-loading-msg"><div class="cgv-lspin"></div><br>Please wait for chat to load</div>
        <div class="cgv-pc">
          ${this._card('enabled', 'Enable Optimise', 'Unload off-screen items', s.enabled, 'main')}
          ${this._card('alwaysExpand', 'Always Expanded', 'Prevent pill from collapsing', s.alwaysExpand, 'dep')}
          ${this._card('topCenter', 'Top Center Position', 'Move the indicator to the top', s.topCenter, 'dep')}
          ${this._slider('uiAlpha', 'UI Alpha', 'Adjust Indicator Transparency', s.uiAlpha, 'dep')}
          ${this._card('instantNewChat', 'Instant New Chats', 'Skip optimisation on new chats', s.instantNewChat, 'dep')}
          ${this._card('aggressive', 'Aggressive Mode', 'Tighter margins, more unloading', s.aggressive, 'dep')}
          ${this._card('debug', 'Debug Mode', 'Highlight unloaded elements', s.debug, 'dep')}
        </div>
        <div class="cgv-ft"><a href="https://github.com/YashRana738/GptOptimum" target="_blank" style="color:inherit;text-decoration:none;opacity:0.7">v2.1 by Yash Rana</a></div>`;

            // Wire toggles
            const enabledTgl = $('[data-key="enabled"] input', el);
            const expandTgl = $('[data-key="alwaysExpand"] input', el);
            const topTgl = $('[data-key="topCenter"] input', el);
            const alphaSld = $('[data-key="uiAlpha"] input', el);
            const instantTgl = $('[data-key="instantNewChat"] input', el);
            const aggrTgl = $('[data-key="aggressive"] input', el);
            const debugTgl = $('[data-key="debug"] input', el);
            const deps = $$('.dep', el);

            const syncDeps = () => {
                deps.forEach(c => c.classList.toggle('off', !enabledTgl.checked));
            };
            syncDeps();

            enabledTgl.addEventListener('change', () => {
                const on = enabledTgl.checked;
                cfg.set('enabled', on);
                virt.setEnabled(on);
                syncDeps();
                if (on && isChatRoute()) {
                    this.busy = true;
                    this.tracked.clear();
                    this.scanMessages();
                    this._optimise();
                } else if (!on) {
                    this.busy = false;
                    this._unlock();
                }
                this._sync(on);
            });

            expandTgl.addEventListener('change', () => {
                const on = expandTgl.checked;
                cfg.set('alwaysExpand', on);
                if (on) {
                    clearTimeout(this.collapseTimer);
                    this.pill?.classList.remove('mini');
                } else {
                    this._scheduleCollapse(500);
                }
            });

            topTgl.addEventListener('change', () => {
                const on = topTgl.checked;
                cfg.set('topCenter', on);
                this.wrap?.classList.toggle('cgv-top-pos', on);
                this.blocker?.classList.toggle('cgv-top-pos', on);
                syncDeps();
            });

            alphaSld.addEventListener('input', () => {
                const val = alphaSld.value;
                cfg.set('uiAlpha', val);
                document.documentElement.style.setProperty('--cgv-ui-alpha', val);
                const valEl = $('.cgv-range-val', el);
                if (valEl) valEl.textContent = Math.round(val * 100) + '%';
            });

            instantTgl.addEventListener('change', () => {
                cfg.set('instantNewChat', instantTgl.checked);
            });

            aggrTgl.addEventListener('change', () => {
                cfg.set('aggressive', aggrTgl.checked);
                virt.setAggressive(aggrTgl.checked);
            });

            debugTgl.addEventListener('change', () => {
                cfg.set('debug', debugTgl.checked);
                virt.setDebug(debugTgl.checked);
            });

            // Card click → toggle
            $$('.cgv-card', el).forEach(card => {
                card.addEventListener('click', e => {
                    if (e.target.tagName === 'INPUT' || e.target.classList.contains('cgv-sl')) return;
                    if (card.classList.contains('off')) return;
                    const inp = $('input', card);
                    inp.checked = !inp.checked;
                    inp.dispatchEvent(new Event('change'));
                });
            });

            return el;
        }

        _card(key, label, desc, checked, cls) {
            return `<div class="cgv-card ${cls}" data-key="${key}"><div class="cgv-ci"><span class="cgv-cl">${label}</span><span class="cgv-cd">${desc}</span></div><label class="cgv-sw"><input type="checkbox"${checked ? ' checked' : ''}><span class="cgv-sl"></span></label></div>`;
        }

        _slider(key, label, desc, value, cls) {
            return `<div class="cgv-card ${cls}" data-key="${key}" style="flex-direction:column;align-items:flex-start;cursor:default"><div style="display:flex;justify-content:space-between;width:100%"><div class="cgv-ci"><span class="cgv-cl">${label}</span><span class="cgv-cd">${desc}</span></div><span class="cgv-range-val" style="font-size:11px;font-weight:600;color:var(--cgv-primary)">${Math.round(value * 100)}%</span></div><input type="range" class="cgv-range" min="0" max="1" step="0.05" value="${value}"></div>`;
        }

        // ── Indicator state management ────────────────────────────
        _sync(enabled) {
            if (!this.pill) return;
            const lbl = $('.cgv-label', this.pill);

            this.pill.classList.remove('busy', 'dead', 'mini', 'pushed');
            clearTimeout(this.collapseTimer);
            this.blocker?.classList.remove('on');

            // Update panel loading state
            this.panel?.classList.toggle('loading', this.busy);

            if (this.busy && virt.enabled) {
                lbl.textContent = 'Optimising';
                this.pill.classList.add('busy', 'on');
                return;
            }

            // Always show the pill — green when enabled, red dot when disabled
            this.pill.classList.add('on');
            if (enabled) {
                if (!this.statusLock) lbl.textContent = 'Optimised';
                this._scheduleCollapse();
            } else {
                if (!this.statusLock) lbl.textContent = 'Disabled';
                this.pill.classList.add('dead');
                // Collapse to compact dot when disabled
                this._scheduleCollapse(800);
            }
        }

        _showBusy() {
            if (!this.pill) return;
            const lbl = $('.cgv-label', this.pill);
            lbl.textContent = 'Optimising';
            this.pill.classList.remove('mini', 'dead');
            this.pill.classList.add('busy', 'on');
            this.panel?.classList.add('loading');
        }

        _scheduleCollapse(delay = 4000) {
            clearTimeout(this.collapseTimer);
            if (cfg.get('alwaysExpand')) return;
            this.collapseTimer = setTimeout(() => {
                this.statusLock = false;
                if (!this.busy && this.pill) this.pill.classList.add('mini');
            }, delay);
        }

        // ── Optimisation cycle ────────────────────────────────────
        _optimise(requireClear = false) {
            let ticks = 0, cleared = !requireClear;
            const iv = setInterval(() => {
                ticks++;
                const msgs = $$(MSG_SEL);
                if (requireClear && !cleared) {
                    if (msgs.length === 0 || ticks > 50) { cleared = true; ticks = 0; }
                    return;
                }
                if (msgs.length > 0 || ticks > 600) {
                    clearInterval(iv);
                    if (msgs.length > 0) this.scanMessages();
                    setTimeout(() => {
                        this.busy = false;
                        this.pill?.classList.remove('pushed');
                        this.blocker?.classList.remove('on');
                        this.panel?.classList.remove('loading');
                        this._unlock();
                        this._sync(virt.enabled);
                    }, 400);
                }
            }, 100);
        }

        // ── Background observer for new chats (no blocking) ──────
        _observeNewChat() {
            let ticks = 0;
            const iv = setInterval(() => {
                ticks++;
                const msgs = $$(MSG_SEL);
                if (msgs.length > 0) {
                    clearInterval(iv);
                    this.scanMessages();
                    this._sync(virt.enabled);
                }
                if (ticks > 300) clearInterval(iv); // 30s timeout
            }, 100);
        }

        _unlock() {
            document.documentElement.classList.remove('cgv-loading');
            // Failsafe: Ensure it's removed even if the above fails or script re-runs
            setTimeout(() => document.documentElement.classList.remove('cgv-loading'), 5000);
        }

        // ── Monitor UI persistence ──────────────────────────────
        _guard() {
            new MutationObserver(() => {
                if (!document.getElementById('cgv-host')) {
                    this._buildUI(cfg.all());
                }
            }).observe(document.body, { childList: true });
        }

        // ── Message tracking ──────────────────────────────────────
        scanMessages() { $$(MSG_SEL).forEach(m => this._track(m)); }

        _track(el) {
            if (this.tracked.has(el)) return;
            this.tracked.add(el);
            el.classList.add('cgv-msg');
            virt.observe(el);
        }

        _observe() {
            new MutationObserver(muts => {
                for (const m of muts) {
                    if (m.type !== 'childList') continue;
                    for (const n of m.addedNodes) {
                        if (n.nodeType !== 1) continue;
                        if (n.matches?.(MSG_SEL)) this._track(n);
                        $$(MSG_SEL, n).forEach(c => this._track(c));
                    }
                }
            }).observe(document.body, { childList: true, subtree: true });
        }
    }

    new App().run();
})();