UX fix for Google Gemini - Buttons to Links

Replace Google Gemini's non-standard button behavior with proper <a> links. Allows native browser operations like opening chats in new tabs (Ctrl+Click / Middle Mouse), copying link addresses, and standard right-click menus on chat history, Gems, and new chat buttons.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         UX fix for Google Gemini - Buttons to Links
// @namespace    https://gemini.google.com/
// @version      1.0.0
// @description  Replace Google Gemini's non-standard button behavior with proper <a> links. Allows native browser operations like opening chats in new tabs (Ctrl+Click / Middle Mouse), copying link addresses, and standard right-click menus on chat history, Gems, and new chat buttons.
// @author       Mykyta Shcherbyna
// @license      MIT
// @match        https://gemini.google.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=gemini.google.com
// @grant        none
// @run-at       document-idle
// @supportURL   https://github.com/mshcherbyna99/
// ==/UserScript==

(function() {
    'use strict';

    const CSS_HOVER_CLASS = 'gemini-linkifier-hover-state';

    const styleSheet = document.createElement('style');
    styleSheet.textContent = `
        .${CSS_HOVER_CLASS} {
            background-color: rgba(255, 255, 255, 0.08) !important;
            border-radius: 100px;
            transition: background-color 0.1s ease;
        }
        @media (prefers-color-scheme: light) {
            .${CSS_HOVER_CLASS} {
                background-color: rgba(0, 0, 0, 0.08) !important;
            }
        }
        div.conversation.${CSS_HOVER_CLASS} { border-radius: 16px; }
        side-nav-entry-button.${CSS_HOVER_CLASS} { border-radius: 100px; }
        div.library-item-card.${CSS_HOVER_CLASS} { border-radius: 12px; }
    `;
    document.head.appendChild(styleSheet);

    function liftElement(el) {
        if (!el) return;
        const style = window.getComputedStyle(el);
        if (style.position === 'static') el.style.position = 'relative';
        el.style.zIndex = '10';
    }

    function createGhostLink(href, visualTarget) {
        const link = document.createElement('a');
        link.href = href;
        Object.assign(link.style, {
            position: 'absolute', top: '0', left: '0', width: '100%', height: '100%',
            zIndex: '5', textDecoration: 'none', cursor: 'pointer', outline: 'none'
        });

        link.addEventListener('click', (e) => {
            if (e.button === 0 && !e.ctrlKey && !e.metaKey) {
                e.preventDefault();
                link.style.display = 'none';
                const elemBelow = document.elementFromPoint(e.clientX, e.clientY);
                if (elemBelow) elemBelow.click();
                link.style.display = 'block';
            }
        });

        if (visualTarget) {
            link.addEventListener('mouseenter', () => visualTarget.classList.add(CSS_HOVER_CLASS));
            link.addEventListener('mouseleave', () => visualTarget.classList.remove(CSS_HOVER_CLASS));
        }
        return link;
    }

    function processConversations() {
        document.querySelectorAll('div.conversation:not([data-linkified="true"])').forEach(node => {
            const jslog = node.getAttribute('jslog');
            if (!jslog) return;
            const match = jslog.match(/"(?:c_)?([a-f0-9]+)"/);
            if (!match) return;

            node.style.position = 'relative';
            const link = createGhostLink(`/app/${match[1]}`, node);
            liftElement(node.querySelector('.conversation-actions-container'));
            node.appendChild(link);
            node.dataset.linkified = 'true';
        });
    }

    function processGemsList() {
        document.querySelectorAll('div.bot-item:not([data-linkified="true"])').forEach(node => {
            const jslog = node.getAttribute('jslog');
            if (!jslog) return;
            const match = jslog.match(/"([^"]+)",(?:null|\d)/);
            if (!match) return;

            node.style.position = 'relative';
            const visual = node.querySelector('button.bot-new-conversation-button') || node;
            const link = createGhostLink(`/gem/${match[1]}`, visual);
            liftElement(node.querySelector('bot-actions-menu'));
            node.appendChild(link);
            node.dataset.linkified = 'true';
        });
    }

    function processStandardNav(selector, url) {
        const container = document.querySelector(selector);
        if (!container || container.dataset.linkified === 'true') return;
        const visual = container.querySelector('.side-nav-entry-button') || container;
        container.style.position = 'relative';
        container.appendChild(createGhostLink(url, visual));
        container.dataset.linkified = 'true';
    }

    function processNewChat() {
        const container = document.querySelector('side-nav-action-button[data-test-id="new-chat-button"]');
        if (!container || container.dataset.linkified === 'true') return;
        const btn = container.querySelector('button[data-test-id="expanded-button"]');
        if (!btn) return;
        container.style.position = 'relative';
        container.appendChild(createGhostLink('/app', btn));
        liftElement(container.querySelector('button[data-test-id="temp-chat-button"]'));
        container.dataset.linkified = 'true';
    }

    function scanAll() {
        processConversations();
        processNewChat();
        processGemsList();
        processStandardNav('side-nav-entry-button[data-test-id="bot-list-side-nav-entry-button"]', '/gems/view');
        processStandardNav('side-nav-entry-button[data-test-id="my-stuff-side-nav-entry-button"]', '/mystuff');
    }

    let timeout;
    const observer = new MutationObserver((mutations) => {
        if (mutations.some(m => m.addedNodes.length > 0)) {
            clearTimeout(timeout);
            timeout = setTimeout(scanAll, 300);
        }
    });

    scanAll();
    observer.observe(document.body, { childList: true, subtree: true });

})();