Automatically updates the browser tab title to the name of the current conversation.
// ==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();
})();