Gemini - Dynamic Tab Title

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

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.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

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 - 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();
})();