Gemini Helper

Gemini enhancements migrated from Voyager — includes copy dollar-sign removal, chat width, default model, and timeline

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Gemini Helper
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Gemini enhancements migrated from Voyager — includes copy dollar-sign removal, chat width, default model, and timeline
// @match        https://gemini.google.com/*
// @license      MIT
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const TAG = '[GeminiHelper]';
    console.log(TAG, 'Script loaded');

    // ═══════════════════════════════════════════════════════════════
    // Trusted Types policy for safe DOM manipulation
    // ═══════════════════════════════════════════════════════════════
    const ghPolicy = (typeof window.trustedTypes !== 'undefined' && window.trustedTypes.createPolicy)
        ? window.trustedTypes.createPolicy('gemini-helper', {
            createHTML: (s) => s,
        })
        : { createHTML: (s) => s };

    function setStyleContent(styleEl, css) {
        // Style elements need special handling under Trusted Types
        try {
            styleEl.textContent = css;
        } catch {
            // Fallback: use sheet API or trusted innerHTML
            styleEl.innerHTML = ghPolicy.createHTML(css);
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // Settings (persisted in localStorage)
    // ═══════════════════════════════════════════════════════════════
    const SETTINGS_KEY = 'geminiHelperSettings';
    function loadSettings() {
        try {
            return JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {};
        } catch { return {}; }
    }
    function saveSettings(s) {
        localStorage.setItem(SETTINGS_KEY, JSON.stringify(s));
    }
    function getSetting(key, fallback) {
        const s = loadSettings();
        return s[key] !== undefined ? s[key] : fallback;
    }
    function setSetting(key, value) {
        const s = loadSettings();
        s[key] = value;
        saveSettings(s);
    }

    // ═══════════════════════════════════════════════════════════════
    // 1. Strip Dollar Signs from Copy (LaTeX delimiters only)
    // ═══════════════════════════════════════════════════════════════
    function initStripDollars() {
        function stripLatexDelimiters(text) {
            // Remove $$ ... $$ (display math) — strip the delimiters, keep inner content
            text = text.replace(/\$\$([\s\S]*?)\$\$/g, '$1');
            // Remove $ ... $ (inline math) — strip the delimiters, keep inner content
            text = text.replace(/\$([^$\n]+?)\$/g, '$1');
            // Clean up LaTeX backslash commands like \frac → frac
            text = text.replace(/\\([a-zA-Z]+)/g, '$1');
            return text;
        }

        function maybeStrip(text) {
            if (!getSetting('stripDollarsEnabled', true)) return text;
            return stripLatexDelimiters(text);
        }

        document.addEventListener('copy', function (e) {
            if (!getSetting('stripDollarsEnabled', true)) return;
            const selection = window.getSelection().toString();
            if (selection && selection.includes('$')) {
                e.clipboardData.setData('text/plain', stripLatexDelimiters(selection));
                e.preventDefault();
            }
        }, true);

        const origWriteText = navigator.clipboard.writeText.bind(navigator.clipboard);
        navigator.clipboard.writeText = function (text) {
            return origWriteText(maybeStrip(text));
        };

        const origWrite = navigator.clipboard.write.bind(navigator.clipboard);
        navigator.clipboard.write = function (data) {
            const cleaned = data.map(function (item) {
                const types = item.types;
                const blobs = {};
                const promises = types.map(function (type) {
                    return item.getType(type).then(function (blob) {
                        if (type === 'text/plain' || type === 'text/html') {
                            return blob.text().then(function (text) {
                                blobs[type] = new Blob([maybeStrip(text)], { type: type });
                            });
                        }
                        blobs[type] = blob;
                    });
                });
                return Promise.all(promises).then(function () {
                    return new ClipboardItem(blobs);
                });
            });
            return Promise.all(cleaned).then(function (items) {
                return origWrite(items);
            });
        };

        // Floating toggle button for strip-dollars
        function createStripDollarsUI() {
            const enabled = getSetting('stripDollarsEnabled', true);

            const btn = document.createElement('button');
            btn.id = 'gemini-helper-strip-dollars-toggle';
            btn.title = 'Strip LaTeX $ on Copy';
            btn.textContent = '$';
            Object.assign(btn.style, {
                position: 'fixed', bottom: '120px', right: '52px', zIndex: '2147483640',
                width: '36px', height: '36px', borderRadius: '50%', border: 'none',
                background: enabled ? '#1a73e8' : '#5f6368', color: '#fff',
                fontSize: '16px', cursor: 'pointer', boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
                transition: 'background 0.2s', display: 'flex', alignItems: 'center',
                justifyContent: 'center', fontFamily: 'sans-serif', fontWeight: 'bold',
                textDecoration: enabled ? 'line-through' : 'none',
            });

            btn.addEventListener('click', () => {
                const nowEnabled = !getSetting('stripDollarsEnabled', true);
                setSetting('stripDollarsEnabled', nowEnabled);
                btn.style.background = nowEnabled ? '#1a73e8' : '#5f6368';
                btn.style.textDecoration = nowEnabled ? 'line-through' : 'none';
                btn.title = nowEnabled ? 'Strip LaTeX $ on Copy (ON)' : 'Strip LaTeX $ on Copy (OFF)';
            });

            document.body.appendChild(btn);
        }

        if (document.body) {
            createStripDollarsUI();
        } else {
            document.addEventListener('DOMContentLoaded', createStripDollarsUI);
        }

        console.log(TAG, 'Strip dollars hooks installed (LaTeX-aware)');
    }

    // ═══════════════════════════════════════════════════════════════
    // 2. Chat Width Adjuster
    // ═══════════════════════════════════════════════════════════════
    function initChatWidth() {
        const STYLE_ID = 'gemini-helper-chat-width';
        const DEFAULT_PERCENT = 70;
        const MIN_PERCENT = 30;
        const MAX_PERCENT = 100;

        const userSelectors = [
            '.user-query-bubble-container',
            '.user-query-container',
            'user-query-content',
            'user-query',
            'div[aria-label="User message"]',
            'article[data-author="user"]',
            '[data-message-author-role="user"]',
        ];

        const assistantSelectors = [
            'model-response',
            '.model-response',
            'response-container',
            '.response-container',
            '.presented-response-container',
            '[aria-label="Gemini response"]',
            '[data-message-author-role="assistant"]',
            '[data-message-author-role="model"]',
            'article[data-author="assistant"]',
        ];

        const tableSelectors = [
            'table-block',
            '.table-block',
            'table-block .table-block',
            'table-block .table-content',
            '.table-block.new-table-style',
            '.table-block.has-scrollbar',
            '.table-block .table-content',
        ];

        function clamp(value) {
            return Math.min(MAX_PERCENT, Math.max(MIN_PERCENT, Math.round(value)));
        }

        function normalizePercent(value) {
            if (!Number.isFinite(value)) return DEFAULT_PERCENT;
            if (value > MAX_PERCENT) return clamp((value / 1200) * 100);
            return clamp(value);
        }

        function applyWidth(widthPercent) {
            const p = normalizePercent(widthPercent);
            const screenW = screen.availWidth || screen.width || 1920;
            const widthValue = Math.round((p / 100) * screenW) + 'px';

            let style = document.getElementById(STYLE_ID);
            if (!style) {
                style = document.createElement('style');
                style.id = STYLE_ID;
                document.head.appendChild(style);
            }

            const ur = userSelectors.join(',\n    ');
            const ar = assistantSelectors.join(',\n    ');
            const tr = tableSelectors.join(',\n    ');

            setStyleContent(style, `
                .content-wrapper:has(chat-window),
                .main-content:has(chat-window),
                .content-container:has(chat-window),
                .content-container:has(.conversation-container) { max-width: none !important; }

                [role="main"]:has(chat-window),
                [role="main"]:has(.conversation-container) { max-width: none !important; }

                chat-window, .chat-container, chat-window-content,
                .chat-history-scroll-container, .chat-history,
                .conversation-container {
                    max-width: none !important;
                    padding-right: 10px !important;
                    box-sizing: border-box !important;
                }

                main > div:has(user-query),
                main > div:has(model-response),
                main > div:has(.conversation-container) {
                    max-width: none !important;
                    width: 100% !important;
                }

                ${ur} {
                    max-width: ${widthValue} !important;
                    width: min(100%, ${widthValue}) !important;
                    margin-left: auto !important;
                    margin-right: auto !important;
                }

                ${ar} {
                    max-width: ${widthValue} !important;
                    width: min(100%, ${widthValue}) !important;
                    margin-left: auto !important;
                    margin-right: auto !important;
                }

                ${tr} {
                    max-width: ${widthValue} !important;
                    width: min(100%, ${widthValue}) !important;
                    margin-left: auto !important;
                    margin-right: auto !important;
                    box-sizing: border-box !important;
                }

                table-block .table-block,
                .table-block.has-scrollbar,
                .table-block.new-table-style { overflow-x: hidden !important; }

                table-block .table-content,
                .table-block .table-content { width: 100% !important; overflow-x: auto !important; }

                model-response:has(> .deferred-response-indicator),
                .response-container:has(img[src*="sparkle"]),
                main > div:has(img[src*="sparkle"]) {
                    max-width: ${widthValue} !important;
                    width: min(100%, ${widthValue}) !important;
                    margin-left: auto !important;
                    margin-right: auto !important;
                }

                user-query, user-query > *, user-query > * > *,
                model-response, model-response > *, model-response > * > *,
                response-container, response-container > *, response-container > * > * {
                    max-width: ${widthValue} !important;
                }

                .presented-response-container,
                [data-message-author-role] { max-width: ${widthValue} !important; }

                input-container { max-width: none !important; width: 100% !important; }

                input-container .input-area-container,
                input-container input-area-v2 {
                    max-width: ${widthValue} !important;
                    width: min(100%, ${widthValue}) !important;
                    margin-left: auto !important;
                    margin-right: auto !important;
                }

                .user-query-bubble-with-background {
                    max-width: ${widthValue} !important;
                    width: fit-content !important;
                }
            `);
        }

        function removeStyles() {
            const el = document.getElementById(STYLE_ID);
            if (el) el.remove();
        }

        // Create slider UI
        function createWidthUI() {
            const enabled = getSetting('chatWidthEnabled', false);
            const percent = normalizePercent(getSetting('chatWidthPercent', DEFAULT_PERCENT));

            if (enabled) applyWidth(percent);

            // Floating toggle button
            const btn = document.createElement('button');
            btn.id = 'gemini-helper-width-toggle';
            btn.title = 'Chat Width';
            btn.textContent = '↔';
            Object.assign(btn.style, {
                position: 'fixed', bottom: '80px', right: '52px', zIndex: '2147483640',
                width: '36px', height: '36px', borderRadius: '50%', border: 'none',
                background: enabled ? '#1a73e8' : '#5f6368', color: '#fff',
                fontSize: '16px', cursor: 'pointer', boxShadow: '0 2px 8px rgba(0,0,0,0.2)',
                transition: 'background 0.2s', display: 'flex', alignItems: 'center',
                justifyContent: 'center', fontFamily: 'sans-serif',
            });

            let sliderContainer = null;

            btn.addEventListener('click', () => {
                if (sliderContainer) {
                    sliderContainer.remove();
                    sliderContainer = null;
                    return;
                }
                sliderContainer = document.createElement('div');
                Object.assign(sliderContainer.style, {
                    position: 'fixed', bottom: '120px', right: '52px', zIndex: '2147483641',
                    background: '#fff', borderRadius: '12px', padding: '16px',
                    boxShadow: '0 4px 24px rgba(0,0,0,0.15)', width: '220px',
                    fontFamily: 'Google Sans, Roboto, sans-serif',
                });

                // Dark mode detection
                const isDark = document.querySelector('.theme-host.dark-theme') ||
                    window.matchMedia('(prefers-color-scheme: dark)').matches;
                if (isDark) {
                    Object.assign(sliderContainer.style, {
                        background: '#1e1e2e', color: '#e2e8f0',
                    });
                }

                const label = document.createElement('div');
                label.style.cssText = 'font-size:13px;font-weight:500;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;';

                const labelText = document.createElement('span');
                labelText.textContent = 'Chat Width';

                const toggle = document.createElement('input');
                toggle.type = 'checkbox';
                toggle.checked = getSetting('chatWidthEnabled', false);
                toggle.style.cssText = 'cursor:pointer;';
                toggle.addEventListener('change', () => {
                    setSetting('chatWidthEnabled', toggle.checked);
                    btn.style.background = toggle.checked ? '#1a73e8' : '#5f6368';
                    if (toggle.checked) {
                        applyWidth(parseInt(slider.value));
                    } else {
                        removeStyles();
                    }
                });

                label.appendChild(labelText);
                label.appendChild(toggle);

                const valDisplay = document.createElement('div');
                valDisplay.style.cssText = 'font-size:12px;color:#888;text-align:center;margin-bottom:4px;';
                const cur = getSetting('chatWidthPercent', DEFAULT_PERCENT);
                valDisplay.textContent = cur + '%';

                const slider = document.createElement('input');
                slider.type = 'range';
                slider.min = String(MIN_PERCENT);
                slider.max = String(MAX_PERCENT);
                slider.value = String(cur);
                slider.style.cssText = 'width:100%;cursor:pointer;';
                slider.addEventListener('input', () => {
                    valDisplay.textContent = slider.value + '%';
                    if (getSetting('chatWidthEnabled', false)) {
                        applyWidth(parseInt(slider.value));
                    }
                });
                slider.addEventListener('change', () => {
                    setSetting('chatWidthPercent', parseInt(slider.value));
                });

                sliderContainer.appendChild(label);
                sliderContainer.appendChild(valDisplay);
                sliderContainer.appendChild(slider);

                // Close on outside click
                const closeHandler = (e) => {
                    if (sliderContainer && !sliderContainer.contains(e.target) && e.target !== btn) {
                        sliderContainer.remove();
                        sliderContainer = null;
                        document.removeEventListener('pointerdown', closeHandler);
                    }
                };
                setTimeout(() => document.addEventListener('pointerdown', closeHandler), 0);

                document.body.appendChild(sliderContainer);
            });

            document.body.appendChild(btn);

            // Re-apply on DOM changes
            let debounce = null;
            const observer = new MutationObserver(() => {
                if (debounce) clearTimeout(debounce);
                debounce = setTimeout(() => {
                    if (getSetting('chatWidthEnabled', false)) {
                        applyWidth(getSetting('chatWidthPercent', DEFAULT_PERCENT));
                    }
                }, 200);
            });
            const main = document.querySelector('main');
            if (main) observer.observe(main, { childList: true, subtree: true });
        }

        if (document.body) {
            createWidthUI();
        } else {
            document.addEventListener('DOMContentLoaded', createWidthUI);
        }
    }

    // ═══════════════════════════════════════════════════════════════
    // 3. Default Model Auto-Selector
    // ═══════════════════════════════════════════════════════════════
    function initDefaultModel() {
        const MODE_ITEM_SELECTOR = '[role="menuitemradio"], [role="menuitem"]';
        const NON_MODEL_MENU_EXCLUSION = '.mat-mdc-menu-panel[role="menu"]:not(.desktop-settings-menu)';
        const FAST_MODEL_IDS = new Set(['56fdd199312815e2']);
        const FAST_MODEL_NAMES = ['flash', '2.0 flash', 'gemini 2.0 flash', 'fast'];
        const CHAT_INPUT_SELECTORS = [
            'main rich-textarea [contenteditable="true"]',
            'rich-textarea [contenteditable="true"]',
            'main div[contenteditable="true"][role="textbox"]',
            'div[contenteditable="true"][role="textbox"]',
            'main .input-area textarea',
            '.input-area textarea',
            'main [contenteditable="true"]',
            'main textarea',
        ];

        const STAR_PATH = 'M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z';
        function createStarSVG(filled) {
            const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svg.setAttribute('viewBox', '0 0 24 24');
            const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
            path.setAttribute('d', STAR_PATH);
            if (filled) {
                path.setAttribute('fill', 'currentColor');
            } else {
                path.setAttribute('fill', 'none');
                path.setAttribute('stroke', 'currentColor');
                path.setAttribute('stroke-width', '1.5');
            }
            svg.appendChild(path);
            return svg;
        }

        let currentDefault = null; // { id, name } or null
        let isLocked = false;
        let checkTimer = null;
        let autoSelectSessionId = null;
        let consecutiveFailures = 0;
        let lastCheckedPath = null;
        let originalPushState = null;
        let originalReplaceState = null;

        // Inject star button CSS
        const starStyle = document.createElement('style');
        setStyleContent(starStyle, `
            .gh-default-star-btn {
                background: transparent; border: none; cursor: pointer; padding: 2px;
                width: 20px; height: 20px; border-radius: 50%; color: #5f6368;
                position: relative; margin-left: 6px; display: flex;
                align-items: center; justify-content: center; z-index: 100;
                pointer-events: auto; opacity: 0;
                transition: opacity 0.2s, background-color 0.2s, color 0.2s;
            }
            [role='menuitemradio']:hover .gh-default-star-btn,
            [role='menuitem']:hover .gh-default-star-btn,
            .gh-default-star-btn.is-default { opacity: 1; }
            .gh-default-star-btn:hover { background-color: rgba(60,64,67,0.08); }
            .gh-default-star-btn.is-default { color: #fbbc04; }
            .gh-default-star-btn svg { width: 14px; height: 14px; pointer-events: none; }
            [role='menuitemradio'], [role='menuitem'] { position: relative; }
        `);
        document.head.appendChild(starStyle);

        function loadDefault() {
            try {
                const raw = localStorage.getItem('geminiHelperDefaultModel');
                if (!raw) return null;
                const parsed = JSON.parse(raw);
                if (typeof parsed === 'string') return { id: null, name: parsed };
                if (parsed && parsed.name) return parsed;
            } catch {}
            return null;
        }

        function saveDefault(model) {
            if (model) {
                localStorage.setItem('geminiHelperDefaultModel', JSON.stringify(model));
            } else {
                localStorage.removeItem('geminiHelperDefaultModel');
            }
        }

        function getModelName(item) {
            const el = item.querySelector('.mode-title, .gds-title-m, .gds-label-l');
            return el ? el.textContent.trim() : '';
        }

        function getModelId(item) {
            const raw = item.getAttribute('data-mode-id') || item.dataset.modeId;
            if (raw && raw.trim()) return raw.trim();
            const jslog = item.getAttribute('jslog');
            if (jslog) {
                const ids = jslog.match(/[a-f0-9]{16}/gi);
                if (ids && ids.length) return ids[ids.length - 1].trim();
            }
            return null;
        }

        function isDefault(item, modelName) {
            if (!currentDefault) return false;
            if (currentDefault.id) {
                const id = getModelId(item);
                return id === currentDefault.id;
            }
            return currentDefault.name === modelName;
        }

        function updateStarState(item, modelName) {
            const btn = item.querySelector('.gh-default-star-btn');
            if (!btn) return;
            const def = isDefault(item, modelName);
            btn.classList.toggle('is-default', def);
            btn.replaceChildren(createStarSVG(def));
            btn.title = def ? 'Cancel default model' : 'Set as default model';
        }

        function showToast(message) {
            const toast = document.createElement('div');
            Object.assign(toast.style, {
                position: 'fixed', bottom: '24px', left: '50%', transform: 'translateX(-50%)',
                background: '#323232', color: 'white', padding: '12px 24px', borderRadius: '4px',
                fontSize: '14px', zIndex: '10000', boxShadow: '0 2px 10px rgba(0,0,0,0.2)',
                transition: 'opacity 0.3s',
            });
            toast.textContent = message;
            document.body.appendChild(toast);
            setTimeout(() => {
                toast.style.opacity = '0';
                setTimeout(() => toast.remove(), 300);
            }, 3000);
        }

        function getMenuPanel() {
            return document.querySelector('.mat-mdc-menu-panel.gds-mode-switch-menu[role="menu"]') ||
                document.querySelector('mat-action-list.gds-mode-switch-menu-list') ||
                document.querySelector(NON_MODEL_MENU_EXCLUSION);
        }

        async function waitForMenuPanel(timeout) {
            const start = Date.now();
            while (Date.now() - start < timeout) {
                const panel = getMenuPanel();
                if (panel && panel.isConnected) return panel;
                await new Promise(r => setTimeout(r, 50));
            }
            return null;
        }

        function injectStarButtons(menuPanel) {
            const items = menuPanel.querySelectorAll(MODE_ITEM_SELECTOR);
            if (!items.length) return false;

            const isModelMenu = menuPanel.querySelector('[data-mode-id]') ||
                menuPanel.querySelector('.mode-title') ||
                menuPanel.querySelector('.title-and-description');
            if (!isModelMenu) return false;

            items.forEach(item => {
                const modelName = getModelName(item);
                if (!modelName) return;

                if (item.querySelector('.gh-default-star-btn')) {
                    updateStarState(item, modelName);
                    return;
                }

                const btn = document.createElement('button');
                btn.className = 'gh-default-star-btn';
                btn.appendChild(createStarSVG(false));
                btn.title = 'Set as default model';

                btn.addEventListener('mousedown', e => e.stopPropagation());
                btn.addEventListener('click', e => {
                    e.stopPropagation();
                    e.preventDefault();

                    const isCurrentlyDefault = isDefault(item, modelName);
                    const modelId = getModelId(item);

                    if (isCurrentlyDefault) {
                        currentDefault = null;
                        saveDefault(null);
                        showToast('Default model cleared');
                    } else {
                        currentDefault = { id: modelId, name: modelName };
                        saveDefault(currentDefault);
                        showToast('Default model set to: ' + modelName);
                    }

                    // Update all buttons
                    injectStarButtons(menuPanel);
                });

                const titleContainer = item.querySelector('.title-and-description');
                if (titleContainer) {
                    const titleEl = titleContainer.querySelector('.mode-title, .gds-title-m, .gds-label-l');
                    if (titleEl) {
                        let wrapper = titleContainer.querySelector('.gh-title-wrapper');
                        if (!wrapper && titleEl.parentElement && titleEl.parentElement.classList.contains('gh-title-wrapper')) {
                            wrapper = titleEl.parentElement;
                        }
                        if (!wrapper) {
                            wrapper = document.createElement('div');
                            wrapper.className = 'gh-title-wrapper';
                            wrapper.style.cssText = 'display:flex;align-items:center;width:100%;';
                            if (titleEl.parentElement) {
                                titleEl.parentElement.insertBefore(wrapper, titleEl);
                            } else {
                                titleContainer.appendChild(wrapper);
                            }
                            wrapper.appendChild(titleEl);
                        }
                        wrapper.appendChild(btn);
                    } else {
                        titleContainer.appendChild(btn);
                    }
                } else {
                    item.appendChild(btn);
                }

                updateStarState(item, modelName);
            });

            return true;
        }

        function isNewConversation() {
            const path = window.location.pathname;
            return /^\/(u\/\d+\/)?(app\/?|gem\/.*)$/.test(path);
        }

        function isFastModel(model) {
            if (model.id && FAST_MODEL_IDS.has(model.id)) return true;
            const name = model.name.toLowerCase().trim();
            return FAST_MODEL_NAMES.some(f => name === f || name.includes(f));
        }

        function findChatInput() {
            for (const sel of CHAT_INPUT_SELECTORS) {
                const els = document.querySelectorAll(sel);
                for (const el of els) {
                    if (!el.isConnected) continue;
                    if (el.tagName === 'TEXTAREA' && el.disabled) continue;
                    return el;
                }
            }
            return null;
        }

        async function tryLockToModel(target) {
            const normalize = s => s.toLowerCase().trim();
            const escape = s => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            const targetName = normalize(target.name);
            const targetWord = new RegExp('(^|\\b)' + escape(targetName) + '(\\b|$)', 'i');

            const selectorBtn = document.querySelector('.input-area-switch-label') ||
                document.querySelector('[data-test-id="model-selector"]') ||
                document.querySelector('button[aria-haspopup="menu"].mat-mdc-menu-trigger');
            if (!selectorBtn) return;

            const currentText = normalize(selectorBtn.textContent || '');
            if (targetWord.test(currentText) || currentText === targetName) {
                if (checkTimer) { clearInterval(checkTimer); checkTimer = null; }
                return;
            }

            if (isLocked) return;
            isLocked = true;

            try {
                selectorBtn.click();
                const menuPanel = await waitForMenuPanel(1500);
                if (!menuPanel) return;

                const items = menuPanel.querySelectorAll(MODE_ITEM_SELECTOR);
                let found = false;
                let switched = false;

                // Try by ID first
                if (target.id) {
                    const item = Array.from(items).find(el => getModelId(el) === target.id);
                    if (item) {
                        const selected = item.getAttribute('aria-checked') === 'true' || item.classList.contains('is-selected');
                        if (!selected) { item.click(); switched = true; }
                        else document.body.click();
                        found = true;
                    }
                }

                // Try by name
                if (!found) {
                    for (const item of items) {
                        if (normalize(getModelName(item)) === targetName) {
                            const selected = item.getAttribute('aria-checked') === 'true' || item.classList.contains('is-selected');
                            if (!selected) { item.click(); switched = true; }
                            else document.body.click();
                            found = true;
                            break;
                        }
                    }
                }

                // Fallback: whole-word match on full text
                if (!found) {
                    for (const item of items) {
                        if (targetWord.test(normalize(item.textContent || ''))) {
                            const selected = item.getAttribute('aria-checked') === 'true' || item.classList.contains('is-selected');
                            if (!selected) { item.click(); switched = true; }
                            else document.body.click();
                            found = true;
                            break;
                        }
                    }
                }

                if (found && checkTimer) {
                    clearInterval(checkTimer);
                    checkTimer = null;
                    consecutiveFailures = 0;
                }

                if (switched) {
                    setTimeout(() => {
                        const input = findChatInput();
                        if (input) input.focus({ preventScroll: true });
                    }, 120);
                }

                if (!found) {
                    document.body.click();
                    consecutiveFailures++;
                    if (consecutiveFailures >= 3 && checkTimer) {
                        clearInterval(checkTimer);
                        checkTimer = null;
                    }
                }
            } catch (e) {
                console.error(TAG, 'Auto lock failed:', e);
            } finally {
                isLocked = false;
            }
        }

        function checkAndLockModel() {
            if (!isNewConversation()) return;
            lastCheckedPath = window.location.pathname;

            currentDefault = loadDefault();
            if (!currentDefault) return;
            if (isFastModel(currentDefault)) return;

            const sessionId = window.location.pathname + '-' + Date.now();
            autoSelectSessionId = sessionId;
            consecutiveFailures = 0;

            let attempts = 0;
            if (checkTimer) clearInterval(checkTimer);
            checkTimer = setInterval(() => {
                if (autoSelectSessionId !== sessionId) { clearInterval(checkTimer); checkTimer = null; return; }
                if (++attempts > 20) { clearInterval(checkTimer); checkTimer = null; return; }
                tryLockToModel(currentDefault);
            }, 1000);
        }

        async function checkAndLockWithDelay() {
            await new Promise(r => setTimeout(r, 150));
            checkAndLockModel();
        }

        // Watch for menu panels being added to the DOM -> inject star buttons
        currentDefault = loadDefault();

        const pendingPanels = new WeakSet();
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                for (const node of mutation.addedNodes) {
                    if (!(node instanceof HTMLElement)) continue;
                    const panel = node.matches('.mat-mdc-menu-panel.gds-mode-switch-menu[role="menu"]') ? node :
                        node.matches('mat-action-list.gds-mode-switch-menu-list') ? node :
                        node.matches(NON_MODEL_MENU_EXCLUSION) ? node :
                        node.querySelector('.mat-mdc-menu-panel.gds-mode-switch-menu[role="menu"]') ||
                        node.querySelector('mat-action-list.gds-mode-switch-menu-list') ||
                        node.querySelector(NON_MODEL_MENU_EXCLUSION);
                    if (panel && !pendingPanels.has(panel)) {
                        pendingPanels.add(panel);
                        setTimeout(() => {
                            pendingPanels.delete(panel);
                            injectStarButtons(panel);
                        }, 50);
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });

        // Initial check + SPA navigation hooks
        checkAndLockModel();

        originalPushState = history.pushState;
        originalReplaceState = history.replaceState;
        history.pushState = function () {
            originalPushState.apply(history, arguments);
            checkAndLockWithDelay();
        };
        history.replaceState = function () {
            originalReplaceState.apply(history, arguments);
            checkAndLockWithDelay();
        };
        window.addEventListener('popstate', () => checkAndLockWithDelay());

        // Sidebar click detection
        document.addEventListener('click', e => {
            const link = e.target.closest('a[href*="/app"]') || e.target.closest('a[href*="/gem/"]');
            if (link) checkAndLockWithDelay();
        }, true);

        // Fallback periodic check
        setInterval(() => {
            const path = window.location.pathname;
            if (path !== lastCheckedPath && isNewConversation()) {
                lastCheckedPath = path;
                checkAndLockModel();
            }
        }, 500);

        console.log(TAG, 'Default model hooks installed');
    }

    // ═══════════════════════════════════════════════════════════════
    // 4. Timeline - Visual conversation navigator
    // ═══════════════════════════════════════════════════════════════
    function initTimeline() {
        const USER_TURN_SELECTORS = [
            '.user-query-bubble-with-background',
            '.user-query-bubble-container',
            '.user-query-container',
            'user-query-content .user-query-bubble-with-background',
            'div[aria-label="User message"]',
            'article[data-author="user"]',
            '[data-message-author-role="user"]',
        ];

        const TIMELINE_STYLE_ID = 'gemini-helper-timeline-style';
        let timelineBar = null;
        let trackContent = null;
        let previewPanel = null;
        let scrollContainer = null;
        let convContainer = null;
        let userTurnSelector = '';
        let markers = [];
        let activeTurnId = null;
        let scrollRafId = null;
        let mutationObs = null;
        let intersectionObs = null;
        let activeUpdateTimer = null;
        let visibleTurns = new Set();
        let destroyed = false;
        let isScrollingProgrammatic = false;
        let scrollingTimer = null;

        function injectStyles() {
            if (document.getElementById(TIMELINE_STYLE_ID)) return;
            const style = document.createElement('style');
            style.id = TIMELINE_STYLE_ID;
            setStyleContent(style, `
                :root {
                    --tl-dot-color: #94a3b8;
                    --tl-dot-active: oklch(0.55 0.17 155);
                    --tl-star-color: #f59e0b;
                    --tl-tip-bg: #ffffff;
                    --tl-tip-text: #0f172a;
                    --tl-tip-border: #e2e8f0;
                    --tl-bar-bg: rgba(248,250,252,0.88);
                    --tl-dot-size: 12px;
                    --tl-active-ring: 3px;
                    --tl-track-pad: 16px;
                    --tl-hit: 30px;
                }
                @media (prefers-color-scheme: dark) {
                    :root {
                        --tl-dot-color: #475569;
                        --tl-dot-active: oklch(0.7 0.16 155);
                        --tl-tip-bg: #0b1220;
                        --tl-tip-text: #e2e8f0;
                        --tl-tip-border: #1f2937;
                        --tl-bar-bg: rgba(2,6,23,0.75);
                    }
                }
                .theme-host.dark-theme {
                    --tl-dot-color: #475569;
                    --tl-dot-active: oklch(0.7 0.16 155);
                    --tl-tip-bg: #0b1220;
                    --tl-tip-text: #e2e8f0;
                    --tl-tip-border: #1f2937;
                    --tl-bar-bg: rgba(2,6,23,0.75);
                }
                .theme-host.light-theme {
                    --tl-dot-color: #94a3b8;
                    --tl-dot-active: oklch(0.55 0.17 155);
                    --tl-tip-bg: #ffffff;
                    --tl-tip-text: #0f172a;
                    --tl-tip-border: #e2e8f0;
                    --tl-bar-bg: rgba(248,250,252,0.88);
                }
                .gh-timeline-bar {
                    position: fixed;
                    top: 60px;
                    right: 15px;
                    width: 24px;
                    height: calc(100vh - 100px);
                    z-index: 2147483646;
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                    border-radius: 12px;
                    background-color: var(--tl-bar-bg);
                    backdrop-filter: blur(6px);
                    -webkit-backdrop-filter: blur(6px);
                    overflow: visible;
                    contain: layout;
                    box-shadow: 0 2px 12px oklch(0 0 0 / 0.06);
                    transition: opacity 0.3s;
                }
                .gh-timeline-track {
                    position: relative;
                    width: 100%;
                    height: 100%;
                    overflow: visible;
                }
                .gh-timeline-track-content {
                    position: relative;
                    width: 100%;
                    height: 100%;
                }
                .gh-tl-dot {
                    position: absolute;
                    left: 50%;
                    top: calc(var(--tl-track-pad) + (100% - 2 * var(--tl-track-pad)) * var(--n, 0));
                    transform: translate(-50%, -50%);
                    width: var(--tl-hit);
                    height: var(--tl-hit);
                    background: transparent;
                    border: none;
                    cursor: pointer;
                    padding: 0;
                }
                .gh-tl-dot::after {
                    content: '';
                    position: absolute;
                    left: 50%;
                    top: 50%;
                    width: var(--tl-dot-size);
                    height: var(--tl-dot-size);
                    transform: translate(-50%, -50%);
                    border-radius: 50%;
                    background-color: var(--tl-dot-color);
                    transition: transform 0.15s ease;
                }
                html.dark .gh-tl-dot:not(.active):not(.starred)::after,
                [data-theme='dark'] .gh-tl-dot:not(.active):not(.starred)::after,
                [data-color-scheme='dark'] .gh-tl-dot:not(.active):not(.starred)::after {
                    background: #000;
                }
                .gh-tl-dot:hover::after { transform: translate(-50%, -50%) scale(1.15); }
                .gh-tl-dot:focus { outline: none; }
                .gh-tl-dot:focus-visible::after { box-shadow: 0 0 6px var(--tl-dot-active); }
                .gh-tl-dot.active::after {
                    box-shadow: 0 0 0 var(--tl-active-ring) var(--tl-dot-active),
                                0 0 14px oklch(0.55 0.17 155 / 0.5);
                }
                .gh-tl-dot.starred::after { background-color: var(--tl-star-color); }

                /* Preview panel - shows all messages on hover */
                .gh-tl-preview {
                    position: fixed;
                    z-index: 2147483647;
                    background: var(--tl-tip-bg);
                    color: var(--tl-tip-text);
                    border: 1px solid var(--tl-tip-border);
                    border-radius: 14px;
                    padding: 8px 0;
                    font-size: 13px;
                    line-height: 18px;
                    box-shadow: 0 12px 36px rgba(2,8,23,0.18), 0 3px 8px rgba(2,8,23,0.08);
                    width: 300px;
                    max-height: calc(100vh - 120px);
                    overflow-y: auto;
                    overflow-x: hidden;
                    opacity: 0;
                    pointer-events: none;
                    transition: opacity 160ms cubic-bezier(0.2,0.8,0.2,1);
                    scrollbar-width: thin;
                }
                .gh-tl-preview.visible {
                    opacity: 1;
                    pointer-events: auto;
                }
                .gh-tl-preview-item {
                    display: flex;
                    align-items: flex-start;
                    gap: 8px;
                    padding: 8px 14px;
                    cursor: pointer;
                    transition: background 0.1s;
                    border: none;
                    background: none;
                    width: 100%;
                    text-align: left;
                    color: inherit;
                    font: inherit;
                    line-height: 18px;
                }
                .gh-tl-preview-item:hover {
                    background: var(--tl-tip-border);
                }
                .gh-tl-preview-item.is-active {
                    background: color-mix(in oklch, var(--tl-dot-active) 15%, transparent);
                }
                .gh-tl-preview-num {
                    flex-shrink: 0;
                    width: 22px;
                    height: 22px;
                    border-radius: 50%;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                    font-size: 11px;
                    font-weight: 600;
                    background: var(--tl-dot-color);
                    color: var(--tl-tip-bg);
                }
                .gh-tl-preview-item.is-active .gh-tl-preview-num {
                    background: var(--tl-dot-active);
                }
                .gh-tl-preview-text {
                    flex: 1;
                    min-width: 0;
                    overflow: hidden;
                    display: -webkit-box;
                    -webkit-line-clamp: 2;
                    -webkit-box-orient: vertical;
                    word-break: break-word;
                }
                .gh-tl-preview::-webkit-scrollbar { width: 4px; }
                .gh-tl-preview::-webkit-scrollbar-thumb {
                    background: var(--tl-dot-color); border-radius: 2px;
                }
            `);
            document.head.appendChild(style);
        }

        function findElements() {
            let firstTurn = null;
            let matchedSel = '';
            for (const sel of USER_TURN_SELECTORS) {
                const el = document.querySelector(sel);
                if (el) { firstTurn = el; matchedSel = sel; break; }
            }
            if (!firstTurn) {
                convContainer = document.querySelector('main') || document.body;
                userTurnSelector = USER_TURN_SELECTORS.join(',');
            } else {
                const isAngular = /user-query/i.test(matchedSel);
                convContainer = isAngular ? (document.querySelector('main') || document.body) : (firstTurn.parentElement || document.body);
                userTurnSelector = matchedSel;
            }

            // Find scroll container
            let p = firstTurn || convContainer;
            while (p && p !== document.body) {
                const st = getComputedStyle(p);
                if (st.overflowY === 'auto' || st.overflowY === 'scroll') {
                    scrollContainer = p;
                    break;
                }
                p = p.parentElement;
            }
            if (!scrollContainer) {
                scrollContainer = document.scrollingElement || document.documentElement || document.body;
            }
            return true;
        }

        function extractText(el) {
            const clone = el.cloneNode(true);
            clone.querySelectorAll('.visually-hidden, [aria-hidden="true"]').forEach(n => n.remove());
            let text = (clone.textContent || '').trim();
            text = text.replace(/^[\u200B-\u200F\uFEFF]*(?:you said|you wrote|user message|your prompt|you asked|你说|你写道|用户消息|你的提示|你问)[:\s\uff1a]*/i, '');
            return text.slice(0, 120) || '(empty)';
        }

        function ensureTurnId(el, idx) {
            let id = el.getAttribute('data-turn-id');
            if (!id) {
                id = 'turn-' + idx + '-' + hashStr((el.textContent || '').slice(0, 80));
                el.setAttribute('data-turn-id', id);
            }
            return id;
        }

        function hashStr(s) {
            let h = 2166136261 >>> 0;
            for (let i = 0; i < s.length; i++) {
                h ^= s.charCodeAt(i);
                h = Math.imul(h, 16777619);
            }
            return (h >>> 0).toString(36);
        }

        function filterTopLevel(els) {
            return els.filter((el, i) => {
                for (let j = 0; j < els.length; j++) {
                    if (i !== j && els[j].contains(el) && els[j] !== el) return false;
                }
                return true;
            });
        }

        function buildMarkers() {
            if (!convContainer || !trackContent) return;
            const turns = Array.from(convContainer.querySelectorAll(userTurnSelector));
            if (!turns.length) {
                setTimeout(buildMarkers, 300);
                return;
            }

            // Clear existing dots
            trackContent.querySelectorAll('.gh-tl-dot').forEach(n => n.remove());

            const allEls = filterTopLevel(turns);
            if (!allEls.length) return;

            const firstOffset = allEls[0].offsetTop;
            const lastOffset = allEls.length > 1 ? allEls[allEls.length - 1].offsetTop : firstOffset;
            const span = Math.max(lastOffset - firstOffset, 1);

            markers = allEls.map((el, idx) => {
                const n = allEls.length <= 1 ? 0.5 : (el.offsetTop - firstOffset) / span;
                const id = ensureTurnId(el, idx);
                const summary = extractText(el);

                const dot = document.createElement('button');
                dot.className = 'gh-tl-dot';
                dot.setAttribute('role', 'button');
                dot.setAttribute('aria-label', summary);
                dot.dataset.targetTurnId = id;
                dot.dataset.markerIndex = String(idx);
                dot.style.setProperty('--n', String(Math.max(0, Math.min(1, n))));
                trackContent.appendChild(dot);

                return { id, element: el, summary, n, dotElement: dot };
            });

            // Set active to last visible or last marker
            if (!activeTurnId && markers.length) {
                activeTurnId = markers[markers.length - 1].id;
            }
            updateActiveDots();
            setupIntersectionObserver();
        }

        function updateActiveDots() {
            markers.forEach(m => {
                if (m.dotElement) m.dotElement.classList.toggle('active', m.id === activeTurnId);
            });
            updatePreviewActiveItem();
        }

        function smoothScrollTo(element) {
            isScrollingProgrammatic = true;
            if (scrollingTimer) clearTimeout(scrollingTimer);

            const done = () => {
                scrollContainer.removeEventListener('scrollend', onEnd);
                clearTimeout(scrollingTimer);
                // small extra delay so final intersection entries settle
                scrollingTimer = setTimeout(() => { isScrollingProgrammatic = false; }, 200);
            };
            const onEnd = () => done();
            scrollContainer.addEventListener('scrollend', onEnd, { once: true });

            element.scrollIntoView({ behavior: 'smooth', block: 'center' });

            // fallback if scrollend never fires
            scrollingTimer = setTimeout(() => {
                scrollContainer.removeEventListener('scrollend', onEnd);
                isScrollingProgrammatic = false;
            }, 2000);
        }

        // ── Preview Panel ──
        let previewHideTimer = null;

        function buildPreviewPanel() {
            if (!previewPanel) return;
            // Clear existing items
            while (previewPanel.firstChild) previewPanel.removeChild(previewPanel.firstChild);

            markers.forEach((m, idx) => {
                const item = document.createElement('button');
                item.className = 'gh-tl-preview-item';
                if (m.id === activeTurnId) item.classList.add('is-active');
                item.dataset.markerIndex = String(idx);

                const num = document.createElement('span');
                num.className = 'gh-tl-preview-num';
                num.textContent = String(idx + 1);

                const text = document.createElement('span');
                text.className = 'gh-tl-preview-text';
                text.textContent = m.summary;

                item.appendChild(num);
                item.appendChild(text);

                item.addEventListener('click', () => {
                    activeTurnId = m.id;
                    updateActiveDots();
                    smoothScrollTo(m.element);
                });

                previewPanel.appendChild(item);
            });
        }

        function updatePreviewActiveItem() {
            if (!previewPanel) return;
            const items = previewPanel.querySelectorAll('.gh-tl-preview-item');
            items.forEach((item, idx) => {
                const isActive = markers[idx] && markers[idx].id === activeTurnId;
                item.classList.toggle('is-active', isActive);
                if (isActive && previewPanel.classList.contains('visible')) {
                    // Scroll active item into view within the panel
                    const panelRect = previewPanel.getBoundingClientRect();
                    const itemRect = item.getBoundingClientRect();
                    if (itemRect.top < panelRect.top || itemRect.bottom > panelRect.bottom) {
                        item.scrollIntoView({ block: 'nearest' });
                    }
                }
            });
        }

        function showPreviewPanel() {
            if (!previewPanel || !timelineBar) return;
            if (previewHideTimer) { clearTimeout(previewHideTimer); previewHideTimer = null; }
            buildPreviewPanel();

            const barRect = timelineBar.getBoundingClientRect();
            const panelW = 300;
            let left = barRect.left - 8 - panelW;
            if (left < 8) left = barRect.right + 8;
            const top = barRect.top;
            const maxH = window.innerHeight - top - 20;

            previewPanel.style.left = left + 'px';
            previewPanel.style.top = top + 'px';
            previewPanel.style.maxHeight = maxH + 'px';

            requestAnimationFrame(() => previewPanel.classList.add('visible'));

            // Scroll active item into view
            const activeItem = previewPanel.querySelector('.is-active');
            if (activeItem) setTimeout(() => activeItem.scrollIntoView({ block: 'nearest' }), 20);
        }

        function hidePreviewPanel() {
            if (previewHideTimer) clearTimeout(previewHideTimer);
            previewHideTimer = setTimeout(() => {
                if (previewPanel) previewPanel.classList.remove('visible');
                previewHideTimer = null;
            }, 200);
        }

        function cancelHidePreview() {
            if (previewHideTimer) { clearTimeout(previewHideTimer); previewHideTimer = null; }
        }

        function setupIntersectionObserver() {
            if (intersectionObs) intersectionObs.disconnect();
            if (!scrollContainer) return;

            const root = scrollContainer === document.documentElement || scrollContainer === document.body ? null : scrollContainer;
            intersectionObs = new IntersectionObserver(entries => {
                entries.forEach(entry => {
                    if (entry.isIntersecting) visibleTurns.add(entry.target);
                    else visibleTurns.delete(entry.target);
                });
                if (isScrollingProgrammatic) return;
                // Debounce to avoid rapid active-dot toggling during scroll
                if (activeUpdateTimer) clearTimeout(activeUpdateTimer);
                activeUpdateTimer = setTimeout(() => {
                    for (const m of markers) {
                        if (visibleTurns.has(m.element)) {
                            if (activeTurnId !== m.id) {
                                activeTurnId = m.id;
                                updateActiveDots();
                            }
                            break;
                        }
                    }
                }, 100);
            }, { root, threshold: 0.1 });

            markers.forEach(m => intersectionObs.observe(m.element));
        }

        function scheduleScrollSync() {
            if (scrollRafId) return;
            scrollRafId = requestAnimationFrame(() => {
                scrollRafId = null;
                // Use intersection observer for active tracking, no extra work needed
            });
        }

        function injectUI() {
            injectStyles();

            timelineBar = document.createElement('div');
            timelineBar.className = 'gh-timeline-bar';

            const track = document.createElement('div');
            track.className = 'gh-timeline-track';

            trackContent = document.createElement('div');
            trackContent.className = 'gh-timeline-track-content';

            track.appendChild(trackContent);
            timelineBar.appendChild(track);

            // Preview panel
            previewPanel = document.createElement('div');
            previewPanel.className = 'gh-tl-preview';
            document.body.appendChild(previewPanel);

            // Event: click to navigate
            timelineBar.addEventListener('click', e => {
                const dot = e.target.closest('.gh-tl-dot');
                if (!dot) return;
                const idx = parseInt(dot.dataset.markerIndex);
                const marker = markers[idx];
                if (marker && marker.element) {
                    activeTurnId = marker.id;
                    updateActiveDots();
                    smoothScrollTo(marker.element);
                }
            });

            // Event: hover shows preview panel
            timelineBar.addEventListener('mouseenter', () => showPreviewPanel());
            timelineBar.addEventListener('mouseleave', () => hidePreviewPanel());
            previewPanel.addEventListener('mouseenter', () => cancelHidePreview());
            previewPanel.addEventListener('mouseleave', () => hidePreviewPanel());

            // Event: scroll passthrough
            timelineBar.addEventListener('wheel', e => {
                e.preventDefault();
                scrollContainer.scrollTop += (e.deltaY || 0);
            }, { passive: false });

            // Scroll tracking
            scrollContainer.addEventListener('scroll', scheduleScrollSync, { passive: true });

            document.body.appendChild(timelineBar);
        }

        function initialize() {
            if (destroyed) return;
            // Clean up previous instance
            document.querySelectorAll('.gh-timeline-bar, .gh-tl-tooltip').forEach(el => el.remove());
            if (mutationObs) { mutationObs.disconnect(); mutationObs = null; }
            if (intersectionObs) { intersectionObs.disconnect(); intersectionObs = null; }
            markers = [];
            activeTurnId = null;
            visibleTurns.clear();

            if (!findElements()) return;
            injectUI();
            buildMarkers();

            // Watch for new messages
            mutationObs = new MutationObserver(() => {
                // Debounced rebuild
                clearTimeout(mutationObs._timer);
                mutationObs._timer = setTimeout(() => {
                    if (!destroyed) buildMarkers();
                }, 300);
            });
            mutationObs.observe(convContainer, { childList: true, subtree: true });
        }

        function isConversationRoute(path) {
            return /^\/(?:u\/\d+\/)?(app|gem)(\/|$)/.test(path || location.pathname);
        }

        function destroy() {
            destroyed = true;
            if (mutationObs) { mutationObs.disconnect(); mutationObs = null; }
            if (intersectionObs) { intersectionObs.disconnect(); intersectionObs = null; }
            document.querySelectorAll('.gh-timeline-bar, .gh-tl-preview').forEach(el => el.remove());
            markers = [];
        }

        // SPA navigation handling
        let currentUrl = location.href;
        let initTimer = null;

        function handleUrlChange() {
            if (location.href === currentUrl) return;
            currentUrl = location.href;

            if (initTimer) { clearTimeout(initTimer); initTimer = null; }

            if (isConversationRoute()) {
                destroyed = false;
                initTimer = setTimeout(initialize, 500);
            } else {
                destroy();
            }
        }

        const origPush = history.pushState;
        const origReplace = history.replaceState;
        history.pushState = function () {
            origPush.apply(history, arguments);
            handleUrlChange();
        };
        history.replaceState = function () {
            origReplace.apply(history, arguments);
            handleUrlChange();
        };
        window.addEventListener('popstate', handleUrlChange);

        // Initial setup
        function setup() {
            if (isConversationRoute()) {
                destroyed = false;
                initialize();
            }
        }

        if (document.body) {
            setup();
        } else {
            const readyObs = new MutationObserver(() => {
                if (document.body) { readyObs.disconnect(); setup(); }
            });
            readyObs.observe(document.documentElement, { childList: true });
        }

        // Periodic fallback
        setInterval(() => {
            if (location.href !== currentUrl) handleUrlChange();
        }, 800);

        window.addEventListener('beforeunload', destroy, { once: true });

        console.log(TAG, 'Timeline installed');
    }

    // ═══════════════════════════════════════════════════════════════
    // Bootstrap - Initialize all features
    // ═══════════════════════════════════════════════════════════════
    initStripDollars();

    function onReady() {
        initChatWidth();
        initDefaultModel();
        initTimeline();
        console.log(TAG, 'All features initialized');
    }

    if (document.body) {
        onReady();
    } else {
        document.addEventListener('DOMContentLoaded', onReady);
    }
})();