Gemini - Dynamic Tab Title

Automatically updates the browser tab title to the name of the current conversation. Reliably handles new chats, switching, and page reloads.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Gemini - Dynamic Tab Title
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @date         2025.12.17
// @description  Automatically updates the browser tab title to the name of the current conversation. Reliably handles new chats, switching, and page reloads.
// @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';

    // --- Constants ---
    // Selector for the active chat in the sidebar history
    const SELECTED_CHAT_ITEM_SELECTOR = 'div[data-test-id="conversation"].selected .conversation-title';
    // Regex to determine if we are currently inside a specific chat ID URL
    const CHAT_PAGE_REGEX = /^\/app\/[a-zA-Z0-9]+$/;
    // Polling settings (Gemini is an SPA, titles load asynchronously)
    const TITLE_POLL_MAX_ATTEMPTS = 50;
    const TITLE_POLL_INTERVAL_MS = 200;

    // --- Globals ---
    let originalDocTitle = 'Gemini';
    let activeTitlePollInterval = null;

    // --- Helper Functions ---

    // Check if the current URL path looks like a chat conversation
    function isChatPage() {
        return CHAT_PAGE_REGEX.test(location.pathname);
    }

    // Stop the currently running poll to prevent overlap
    function stopActiveTitlePoll() {
        if (activeTitlePollInterval) {
            clearInterval(activeTitlePollInterval);
            activeTitlePollInterval = null;
        }
    }

    // Main logic: Poll the DOM to find the sidebar title and update the tab
    function startNavigationPoll() {
        stopActiveTitlePoll();

        let pollAttempts = 0;
        let lastKnownTitle = '';

        activeTitlePollInterval = setInterval(() => {
            pollAttempts++;
            const selectedTitleElement = document.querySelector(SELECTED_CHAT_ITEM_SELECTOR);

            if (isChatPage() && selectedTitleElement) {
                const currentTitleText = selectedTitleElement.textContent.trim();

                // Only update if the title is valid and different from what we last saw
                if (currentTitleText && currentTitleText !== lastKnownTitle) {
                    lastKnownTitle = currentTitleText;
                    const newTitle = `${lastKnownTitle} - ${originalDocTitle}`;

                    if (document.title !== newTitle) {
                        document.title = newTitle;
                    }
                }
            }

            // Stop polling if we exceed attempts or leave the chat page
            if (pollAttempts >= TITLE_POLL_MAX_ATTEMPTS || !isChatPage()) {
                // If we left the chat page, ensure the title is reset to "Gemini"
                if (!isChatPage() && document.title !== originalDocTitle) {
                    document.title = originalDocTitle;
                }
                stopActiveTitlePoll();
            }
        }, TITLE_POLL_INTERVAL_MS);
    }

    // Watches for the initial page load to determine when to start polling
    function startInitialLoadWatcher() {
        stopActiveTitlePoll();
        activeTitlePollInterval = setInterval(() => {
            if (isChatPage()) {
                startNavigationPoll();
            }
        }, 500);
    }

    // Hooks into the History API (pushState/replaceState) to detect SPA navigation
    // This is required because Gemini changes URL without reloading the page
    function setupHistoryHookForTitle() {
        const originalPushState = history.pushState;
        const originalReplaceState = history.replaceState;

        const navigationEventHandler = () => {
            startNavigationPoll();
        };

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

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

        window.addEventListener('popstate', () => navigationEventHandler());
    }

    // --- Initialization ---
    function initialize() {
        // Save the default title (usually "Gemini")
        originalDocTitle = document.title || 'Gemini';

        // Set up the listeners for navigation
        setupHistoryHookForTitle();

        // Determine immediate state
        if (isChatPage()) {
            startNavigationPoll();
        } else {
            startInitialLoadWatcher();
        }
    }

    initialize();
})();