Gemini Code Block Scroller (v2.2)

Gemini、ChatGPT代码块滚动增强:标题居中、状态跟随、自动历史补全、双击展开。

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        Gemini Code Block Scroller (v2.2)
// @namespace    http://tampermonkey.net/
// @version      2.2.0
// @description  Gemini、ChatGPT代码块滚动增强:标题居中、状态跟随、自动历史补全、双击展开。
// @author       gymimc
// @match        https://gemini.google.com/*
// @match        https://chatgpt.com/*
// @match        https://chat.openai.com/*
// @grant        GM_addStyle
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const SITE_CONFIGS = {
        // ========================================================
        // 💎 Gemini 专属配置 (彻底剿灭缓存的幽灵按钮)
        // ========================================================
        'gemini': {
            checkMatch: (host) => host.includes('gemini.google.com'),
            selectors: {
                getBlocks: () => document.querySelectorAll('.code-block'),
                getCodeContainer: (block) => block.querySelector('.formatted-code-block-internal-container pre'),
                getHeader: (block) => block.querySelector('.code-block-decoration') || block.querySelector('div:first-child'),
                getLangLabel: (header) => {
                    const spans = Array.from(header.querySelectorAll('span'));
                    if (spans.length === 0) return header.firstElementChild;
                    return spans.find(s => s.textContent.trim().length > 0 && !s.classList.contains('gm-status-badge')) || spans[0];
                },
            },
            layoutMode: 'gemini-center',
            isGenerating: () => {
                // 1. 原封不动保留 2.0 经典且有效的文字模糊匹配
                const buttons = Array.from(document.querySelectorAll('button[aria-label]'));
                const stopBtn = buttons.find(btn => {
                    const label = btn.getAttribute('aria-label');
                    if (!label) return false;
                    
                    const hasStop = label.includes('Stop') || label.includes('停止');
                    const isMedia = label.includes('朗读') || label.includes('收听') || label.includes('播放') || label.includes('Read') || label.includes('Listen');
                    
                    if (hasStop && !isMedia) {
                        // 2. ⭐ X光透视仪:物理尺寸过滤
                        const rect = btn.getBoundingClientRect();
                        if (rect.width < 1 || rect.height < 1) return false;
                        
                        // 3. ⭐ X光透视仪:DOM 树穿透检测!向上查完所有祖宗十八代,只要有一个被透明化或隐藏,直接判定为幽灵按钮!
                        let node = btn;
                        while (node && node !== document.documentElement) {
                            const style = window.getComputedStyle(node);
                            if (style.opacity === '0' || style.visibility === 'hidden' || style.display === 'none') {
                                return false; // 抓到幽灵按钮,踢出!
                            }
                            node = node.parentElement;
                        }
                        
                        // 连过三关,证明这是真正呈现在你眼前的“生成中”按钮
                        return true;
                    }
                    return false;
                });
                return !!stopBtn;
            }
        },
        
        // ========================================================
        // 🚀 ChatGPT 专属配置
        // ========================================================
        'chatgpt': {
            checkMatch: (host) => host.includes('chatgpt.com') || host.includes('chat.openai.com'),
            selectors: {
                getBlocks: () => document.querySelectorAll('.markdown pre'),
                getCodeContainer: (block) => block.querySelector('.cm-scroller') || block.querySelector('code') || block,
                getHeader: (block) => block.querySelector('.justify-between') || block.querySelector('.flex.items-center.justify-between'),
                getLangLabel: (header) => header.querySelector('.justify-self-start') || header.firstElementChild,
            },
            layoutMode: 'chatgpt-inner-append', 
            isGenerating: () => !!document.querySelector('.result-streaming, [data-testid="stop-button"]')
        }
    };

    const host = window.location.hostname;
    let currentSiteName = null;
    let site = Object.values(SITE_CONFIGS).find(s => {
        if (s.checkMatch(host)) {
            currentSiteName = host.includes('gemini') ? 'gemini' : 'chatgpt';
            return true;
        }
        return false;
    });
    
    if (!site) return;

    document.body.classList.add(currentSiteName + '-mode');

    const CONFIG = {
        maxHeight: '350px',
        statusText: { generating: '⏳ 生成中...', done: '✅ 代码生成完毕' }
    };

    // ================= 全局样式引擎 (⚠️ 绝对锁定) =================
    GM_addStyle(`
        /* 核心安全滚动 */
        .gm-scroll-active {
            max-height: ${CONFIG.maxHeight} !important;
            overflow-y: auto !important;
            transition: max-height 0.2s ease-out;
        }
        .gm-scroll-active.gm-expanded {
            max-height: none !important;
            height: auto !important;
        }

        /* 状态标签基础 */
        .gm-status-badge {
            font-size: 12px; padding: 2px 8px; border-radius: 12px;
            font-family: sans-serif; font-weight: 500;
            display: inline-flex; align-items: center; white-space: nowrap; height: 24px;
            margin-left: 8px; 
        }
        .gm-status-badge.generating { background: rgba(253, 214, 99, 0.15); color: #d9a000; border: 1px solid rgba(253, 214, 99, 0.3); animation: pulse 1.5s infinite; }
        .gm-status-badge.done { background: rgba(129, 201, 149, 0.15); color: #2ea043; border: 1px solid rgba(129, 201, 149, 0.3); }
        @keyframes pulse { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } }

        /* === 💎 Gemini 样式 === */
        body.gemini-mode .gm-scroll-active { display: block !important; border-bottom: 2px solid rgba(138, 180, 248, 0.1); padding-bottom: 20px !important; cursor: zoom-in !important;}
        body.gemini-mode .gm-scroll-active.gm-expanded { cursor: zoom-out !important; }
        body.gemini-mode .gm-lang-label-centered { margin-left: auto !important; }
        body.gemini-mode .gm-status-badge { margin-right: auto !important; margin-left: 10px !important; }

        /* === 🚀 ChatGPT 样式 === */
        body.chatgpt-mode .gm-scroll-active,
        body.chatgpt-mode .gm-scroll-active *,
        body.chatgpt-mode .gm-scroll-active .cm-content,
        body.chatgpt-mode .gm-scroll-active .cm-line { 
            cursor: zoom-in !important; 
        }
        body.chatgpt-mode .gm-scroll-active.gm-expanded,
        body.chatgpt-mode .gm-scroll-active.gm-expanded *,
        body.chatgpt-mode .gm-scroll-active.gm-expanded .cm-content,
        body.chatgpt-mode .gm-scroll-active.gm-expanded .cm-line { 
            cursor: zoom-out !important; 
        }
    `);

    // ================= 交互与扫描引擎 (⚠️ 绝对锁定) =================
    document.addEventListener('dblclick', (e) => {
        const pre = e.target.closest('.gm-scroll-active');
        if (pre) {
            e.preventDefault();
            window.getSelection()?.removeAllRanges();
            
            const wasExpanded = pre.classList.contains('gm-expanded');
            pre.classList.toggle('gm-expanded');
            
            // 瞬间视野回弹(解决迷路问题)
            if (wasExpanded) {
                pre.scrollIntoView({ behavior: 'instant', block: 'center' });
            }
            
            if (document.body.classList.contains('chatgpt-mode')) {
                setTimeout(() => window.dispatchEvent(new Event('resize')), 50);
            }
        }
    });

    setInterval(() => {
        const blocks = site.selectors.getBlocks();
        if (blocks.length === 0) return;
        
        const isGenerating = site.isGenerating();

        blocks.forEach((block, index) => {
            const container = site.selectors.getCodeContainer(block);
            if (container && !container.classList.contains('gm-scroll-active')) {
                container.classList.add('gm-scroll-active');
            }

            const header = site.selectors.getHeader(block);
            if (!header) return;

            const lang = site.selectors.getLangLabel(header);
            if (currentSiteName === 'gemini' && lang) {
                lang.classList.add('gm-lang-label-centered');
            }

            let badge = header.querySelector('.gm-status-badge');
            const isLast = index === blocks.length - 1;
            const status = (isGenerating && isLast) ? 'generating' : 'done';
            const text = status === 'generating' ? CONFIG.statusText.generating : CONFIG.statusText.done;

            if (!badge) {
                badge = document.createElement('span');
                badge.className = `gm-status-badge ${status}`;
                
                if (site.layoutMode === 'chatgpt-inner-append' && lang) {
                    lang.appendChild(badge);
                } 
                else if (lang && lang.nextSibling) {
                    header.insertBefore(badge, lang.nextSibling);
                } else {
                    header.appendChild(badge);
                }
            }
            
            if (badge.textContent !== text || !badge.classList.contains(status)) {
                badge.textContent = text;
                badge.className = `gm-status-badge ${status}`;
            }
        });
    }, 500);

    console.log(`🚀 Scroller v4.9 Supreme [${currentSiteName.toUpperCase()}] 已启动`);
})();