Gemini - Dynamic Tab Title

Automatically updates the browser tab title to the name of the current conversation.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Gemini - Dynamic Tab Title
// @namespace    http://tampermonkey.net/
// @version      1.1.0
// @description  Automatically updates the browser tab title to the name of the current conversation.
// @author       Te55eract, JonathanLU, and Gemini
// @match        *://gemini.google.com/*
// @icon         https://upload.wikimedia.org/wikipedia/commons/1/1d/Google_Gemini_icon_2025.svg
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const CHAT_PAGE_REGEX = /^\/app\/[a-zA-Z0-9]+$/;
    const TITLE_POLL_INTERVAL_MS = 500; // Slower poll to save resources
    const DEFAULT_TITLE = 'Gemini';

    // --- State ---
    let activeTitlePollInterval = null;
    let lastKnownTitle = '';

    // --- Helper: Find the Title ---
    // This function attempts 3 strategies to find the title, making it resistant to updates.
    function getActiveChatTitle() {
        try {
            // Strategy 1: Accessibility Attribute (Most Robust)
            // Google uses aria-selected="true" for the active sidebar item for screen readers.
            const ariaSelected = document.querySelector('[role="navigation"] [aria-selected="true"]');
            if (ariaSelected) {
                // We want the text inside, but we need to be careful not to grab "options" button text.
                // Usually the title is the distinct text node or inside a specific div.
                // Let's try to find a title-like element inside the active container.
                const titleSpan = ariaSelected.querySelector('.conversation-title, [data-test-id="conversation-title"]');
                if (titleSpan) return titleSpan.textContent.trim();

                // Fallback: Just grab the text of the container if specific class is missing
                // Clean up newlines to avoid grabbing date headers if the selection is weird
                return ariaSelected.textContent.replace(/[\n\r]+.*/g, '').trim();
            }

            // Strategy 2: URL Matching
            // Find the link in the sidebar that matches the current browser URL
            const currentPath = window.location.pathname;
            if (currentPath.includes('/app/')) {
                const activeLink = document.querySelector(`a[href$="${currentPath}"]`);
                if (activeLink) {
                    return activeLink.textContent.trim();
                }
            }
        } catch (e) {
            // Fail silently
        }
        return null;
    }

    function isChatPage() {
        return CHAT_PAGE_REGEX.test(location.pathname);
    }

    function stopActiveTitlePoll() {
        if (activeTitlePollInterval) {
            clearInterval(activeTitlePollInterval);
            activeTitlePollInterval = null;
        }
    }

    function startNavigationPoll() {
        stopActiveTitlePoll();

        // Immediate check
        updateTitle();

        // Persistent check
        activeTitlePollInterval = setInterval(() => {
            if (!isChatPage()) {
                if (document.title !== DEFAULT_TITLE) document.title = DEFAULT_TITLE;
                return;
            }
            updateTitle();
        }, TITLE_POLL_INTERVAL_MS);
    }

    function updateTitle() {
        const foundTitle = getActiveChatTitle();

        // If we found a title and it's different from the last one we set
        if (foundTitle && foundTitle !== lastKnownTitle) {
            lastKnownTitle = foundTitle;
            const newDocTitle = `${lastKnownTitle} - ${DEFAULT_TITLE}`;
            if (document.title !== newDocTitle) {
                document.title = newDocTitle;
            }
        }
    }

    function setupHistoryHook() {
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        const handleNav = () => {
            // Reset known title on nav so we force a re-check
            lastKnownTitle = '';
            startNavigationPoll();
        };

        history.pushState = function(...args) {
            const res = originalPushState.apply(this, args);
            handleNav();
            return res;
        };

        history.replaceState = function(...args) {
            const res = originalReplaceState.apply(this, args);
            handleNav();
            return res;
        };

        window.addEventListener('popstate', handleNav);
    }

    // --- Init ---
    function initialize() {
        setupHistoryHook();
        startNavigationPoll();
    }

    initialize();
})();