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