Gemini Conversation Folders

Adds folders with a settings menu to the Google Gemini sidebar to organize conversations.

// ==UserScript==
// @name         Gemini Conversation Folders
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Adds folders with a settings menu to the Google Gemini sidebar to organize conversations.
// @author       T. Berkeley Goodloe
// @match        https://gemini.google.com/app*
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @require      https://cdn.jsdelivr.net/npm/[email protected]/Sortable.min.js
// @license      none
// ==/UserScript==

// -----------------------------------------------------------------------------
// Script: Gemini Conversation Folders
// Author: T. Berkeley Goodloe
// Created: 2025-08-07
// Version: 1.0
// Contact: [email protected]
// Description: Sidebar folders for Google Gemini conversations.
//              Adds an inline “Folders + ⚙︎” header.
//
//              ⚙︎ gear menu actions:
//              • Copy Debug Code  → copies a JSON snapshot to the clipboard containing:
//                   – storedFolders (array of folder objects)
//                   – storedConversationFolders (map of conversation‑id → folder‑id)
//                   – domFolders (ids currently in each folder element)
//                   – domOrphans (ids still loose in the main list)
//              • Reset Folder Data  → deletes Tampermonkey keys “gemini_folders” &
//                   “gemini_convo_folders” and reloads the page, returning you to a
//                   fresh empty state.
// -----------------------------------------------------------------------------

(function() {
    'use strict';

    // --- SELECTORS ---
    const CHAT_ITEM_SELECTOR = 'div[data-test-id="conversation"]';
    const CHAT_CONTAINER_SELECTOR = '.conversation-items-container';
    const CHAT_LIST_CONTAINER_SELECTOR = 'conversations-list .conversations-container';
    const INJECTION_POINT_SELECTOR = 'div.chat-history-list';

    // --- STYLES ---
    GM_addStyle(`
    #folder-ui-container { padding: 0 8px; }
    #folder-container { padding-bottom: 8px; border-bottom: 1px solid var(--surface-3); }
    .folder { margin-bottom: 5px; border-radius: 8px; overflow: hidden; }
    .folder-header { display: flex; align-items: center; padding: 10px; cursor: pointer; background-color: var(--surface-2); position: relative; }
    .folder-header:hover { background-color: var(--surface-3); }
    .folder-color-indicator { width: 8px; height: 20px; border-radius: 4px; margin-right: 10px; flex-shrink: 0; }
    .folder-name { flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: 'Roboto', Arial, sans-serif !important; }
    .folder-controls { display: flex; align-items: center; margin-left: 5px; }
    .folder-toggle-icon { transition: transform 0.2s; }
    .folder.closed .folder-toggle-icon { transform: rotate(-90deg); }
    .folder-options-btn { background: none; border: none; color: inherit; cursor: pointer; padding: 2px 4px; border-radius: 4px; margin-left: 4px; font-size: 1.2em; line-height: 1; }
    .folder-options-btn:hover { background-color: rgba(255,255,255,0.1); }
    .folder-content { max-height: 500px; overflow-y: auto; transition: max-height 0.3s ease-in-out, padding 0.3s ease-in-out; background-color: var(--surface-1); }
    .folder.closed .folder-content { max-height: 0; padding-top: 0; padding-bottom: 0; }
    #add-folder-btn { width: 100%; margin: 8px 0; padding: 10px; border: none; background-color: var(--primary-surface); color: var(--on-primary-surface); border-radius: 8px; cursor: pointer; font-weight: 500; }
    #add-folder-btn:hover { opacity: 0.9; }
    .sortable-ghost { opacity: 0.4; background: var(--primary-surface-hover); }
    .conversation-items-container { cursor: grab; }
    .folder-context-menu { position: absolute; z-index: 10000; background-color: #333333; border: 1px solid var(--surface-4); border-radius: 8px; padding: 5px; box-shadow: 0 4px 8px rgba(0,0,0,0.2); display: none; }
    .folder-context-menu-item { padding: 8px 12px; cursor: pointer; border-radius: 4px; white-space: nowrap; font-family: 'Roboto', Arial, sans-serif !important; color: #FFFFFF; }
    .folder-context-menu-item:hover { background-color: var(--surface-4); }
    .folder-context-menu-item.delete { color: #DB4437; }

    /* ---- Color-picker ---- */
    .color-picker-dialog .color-swatch {
        width: 32px;
        height: 32px;
        border-radius: 50%;
        cursor: pointer;
        border: 2px solid transparent;
        position: relative;          /* for the ring ::after */
    }
    .color-picker-dialog .color-swatch:hover {
        border: 2px solid var(--on-primary-surface);
    }
    .color-picker-dialog .color-swatch.selected {
        border: 2px solid transparent;               /* remove hover border */
    }
    .color-picker-dialog .color-swatch.selected::after {
        content: "";
        position: absolute;
        inset: 0;
        border: 3px solid #fff;                      /* ring color / thickness */
        border-radius: 50%;
        box-sizing: border-box;
        pointer-events: none;
    }
        /* ─── Header row ─────────────────── */
    #folders-header{
        display:flex;align-items:center;margin:8px 0 12px;
        font-family:'Roboto', Arial, sans-serif;
    }
    .fh-title   { font-weight:600; }
    .fh-spacer  { flex:1; }
    #fh-add,#fh-gear{
        background:none;border:none;color:inherit;
        font-size:18px;cursor:pointer;margin-left:6px;
        width:28px;height:28px;display:flex;align-items:center;justify-content:center;
        border-radius:4px;
    }
    #fh-add:hover,#fh-gear:hover{ background:rgba(255,255,255,.1); }

    /* ─── Gear pop-over ───────────────── */
    #folder-tools-pop{
        position:absolute;z-index:9999;
        background:#333;border:1px solid var(--surface-4);
        border-radius:8px;padding:6px;box-shadow:0 4px 10px rgba(0,0,0,.3);
    }
    #folder-tools-pop .tools-item{
        display:block;width:100%;text-align:left;
        background:none;border:none;color:#fff;
        padding:8px 14px;cursor:pointer;border-radius:6px;
        font-family:'Roboto', Arial, sans-serif;font-size:14px;
    }
    #folder-tools-pop .tools-item:hover{
        background:var(--surface-4);
    }

    #reset-data-btn { position: fixed; bottom: 15px; right: 15px; z-index: 9999; background-color: #DB4437; color: white; border: none; border-radius: 8px; padding: 10px 15px; font-family: 'Roboto', Arial, sans-serif; font-size: 14px; cursor: pointer; box-shadow: 0 2px 5px rgba(0,0,0,0.3); }
    .custom-dialog-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(34, 34, 34, 0.75); z-index: 20000; display: flex; align-items: center; justify-content: center; }
    .custom-dialog-box { background-color: #333333; padding: 25px; border-radius: 12px; box-shadow: 0 5px 15px rgba(0,0,0,0.3); text-align: center; max-width: 400px; border: 1px solid var(--surface-4); }
    .custom-dialog-box p, .custom-dialog-box h2 { margin: 0 0 20px; font-family: 'Roboto', Arial, sans-serif; color: #FFFFFF; }
    .custom-dialog-btn { border: none; border-radius: 8px; padding: 10px 20px; cursor: pointer; font-weight: 500; margin: 0 10px; }
    .dialog-btn-confirm { background-color: #8ab4f8; color: #202124; }
    .dialog-btn-delete { background-color: #DB4437; color: white; }
    .dialog-btn-cancel { background-color: var(--surface-4); color: var(--on-surface); }
    .custom-dialog-input { width: 100%; box-sizing: border-box; padding: 10px; border-radius: 8px; border: 1px solid var(--surface-4); background-color: var(--surface-1); color: var(--on-surface); font-size: 16px; margin-bottom: 20px; }
    .color-picker-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; margin-bottom: 20px; }
`);

    // --- STATE MANAGEMENT ---
    let folders = [];
    let conversationFolders = {};
    let chatItemCache = new Map();
    const FOLDER_COLORS = ['#370000', '#0D3800', '#001B38', '#383200', '#380031', '#7DAC89', '#7A82AF', '#AC7D98', '#7AA7AF', '#9CA881'];

    async function loadData() {
        folders = await GM_getValue('gemini_folders', []);
        conversationFolders = await GM_getValue('gemini_convo_folders', {});
    }

    async function saveData() {
        await GM_setValue('gemini_folders', folders);
        await GM_setValue('gemini_convo_folders', conversationFolders);
    }

    // ─── UNIQUE-ID DETECTION ─────────────────────────────────────────
    function getIdentifierFromElement(el) {
        if (!el) return null;

        // If we were handed the container, drill down once to the conversation div
        if (el.matches('.conversation-items-container')) {
            el = el.querySelector('[data-test-id="conversation"]') || el;
        }

        /* 1. New URL-style id ( /conversation/<id> )  */
        const anchor = el.closest('a');
        if (anchor) {
            const href = anchor.getAttribute('href') || '';
            const m = href.match(/\/conversation\/([A-Za-z0-9_-]+)/);
            if (m) return m[1];
        }

        /* 2. jslog metadata ( c_<id> )  */
        const jslog = el.getAttribute('jslog') || '';
        let m = jslog.match(/"c_([A-Za-z0-9_-]+)"/); // within quotes
        if (!m) m = jslog.match(/c_([A-Za-z0-9_-]+)/); // bare
        if (m) return m[1];

        /* 3. Fallback to visible title text (last resort) */
        const t = el.querySelector('.conversation-title');
        if (t) return `title:${t.textContent.trim()}`;

        /* 4. Couldn’t identify – log once per element */
        console.warn('[Gemini Folders] Could not find ID for element:', el);
        return null;
    }

    // --- UI RENDERING ---
    function renderFolders() {
        const container = document.getElementById('folder-container');
        if (!container) return;
        while (container.firstChild) container.removeChild(container.firstChild);

        folders.forEach(folder => {
            const folderEl = document.createElement('div');
            folderEl.className = 'folder';
            folderEl.dataset.folderId = folder.id;
            if (folder.isClosed) folderEl.classList.add('closed');

            const headerEl = document.createElement('div');
            headerEl.className = 'folder-header';
            headerEl.addEventListener('click', (e) => {
                if (!e.target.closest('.folder-options-btn')) toggleFolder(folder.id);
            });

            const colorIndicator = document.createElement('div');
            colorIndicator.className = 'folder-color-indicator';
            colorIndicator.style.backgroundColor = folder.color;

            const nameEl = document.createElement('span');
            nameEl.className = 'folder-name';
            nameEl.textContent = folder.name;

            const controlsEl = document.createElement('div');
            controlsEl.className = 'folder-controls';

            const toggleIcon = document.createElement('span');
            toggleIcon.className = 'folder-toggle-icon';
            toggleIcon.textContent = '▼';

            const optionsBtn = document.createElement('button');
            optionsBtn.className = 'folder-options-btn';
            optionsBtn.textContent = '⋮';
            optionsBtn.addEventListener('click', (e) => showContextMenu(e, folder.id));

            controlsEl.appendChild(toggleIcon);
            controlsEl.appendChild(optionsBtn);
            headerEl.appendChild(colorIndicator);
            headerEl.appendChild(nameEl);
            headerEl.appendChild(controlsEl);

            const contentEl = document.createElement('div');
            contentEl.className = 'folder-content';

            folderEl.appendChild(headerEl);
            folderEl.appendChild(contentEl);
            container.appendChild(folderEl);
        });

        organizeConversations();
        setupDragAndDrop();
    }

    // --- FIXED FUNCTION: This is where the magic happens ---
    function organizeConversations() {
        const chatListContainer = document.querySelector(CHAT_LIST_CONTAINER_SELECTOR);
        if (!chatListContainer) return;

        const folderIds = new Set(folders.map(f => f.id));
        let dataWasCorrected = false;

        // Only move orphaned chats back to main (not ALL chats!)
        document.querySelectorAll('.folder-content ' + CHAT_CONTAINER_SELECTOR).forEach(item => {
            const convoEl = item.querySelector(CHAT_ITEM_SELECTOR);
            const identifier = getIdentifierFromElement(convoEl);

            // Only move if chat shouldn't be in any folder
            if (!identifier || !conversationFolders[identifier] || !folderIds.has(conversationFolders[identifier])) {
                chatListContainer.appendChild(item);
            }
        });

        // Process chats in main container and assign to folders
        Array.from(chatListContainer.children).forEach(itemToMove => {
            const convoEl = itemToMove.querySelector(CHAT_ITEM_SELECTOR);
            const identifier = getIdentifierFromElement(convoEl);
            if (!identifier) return;

            let folderId = conversationFolders[identifier];

            // Clean up references to deleted folders
            if (folderId && !folderIds.has(folderId)) {
                delete conversationFolders[identifier];
                folderId = null;
                dataWasCorrected = true;
            }

            // Move to appropriate folder
            if (folderId) {
                const folderContent = document.querySelector(`.folder[data-folder-id="${folderId}"] .folder-content`);
                if (folderContent && !folderContent.contains(itemToMove)) {
                    folderContent.appendChild(itemToMove);
                }
            }
        });

        if (dataWasCorrected) saveData();
    }

    // --- FOLDER ACTIONS ---
    function createNewFolder() {
        showCustomPromptDialog("Enter New Folder Name", "", "Create", (name) => {
            if (name) {
                const newFolder = { id: `folder_${Date.now()}`, name, color: '#808080', isClosed: false };
                folders.push(newFolder);
                saveData().then(renderFolders);
            }
        });
    }
    // === patch-only: updates an existing folder header in place ===
    function updateFolderHeader(folderId) {
        const folder = folders.find(f => f.id === folderId);
        const folderEl = document.querySelector(`.folder[data-folder-id="${folderId}"]`);
        if (!folder || !folderEl) return;

        folderEl.querySelector('.folder-name').textContent = folder.name;
        folderEl.querySelector('.folder-color-indicator').style.backgroundColor = folder.color;
    }
    function renameFolder(folderId) {
        const folder = folders.find(f => f.id === folderId);
        if (!folder) return;

        showCustomPromptDialog("Rename Folder", folder.name, "Save", (newName) => {
            if (newName && newName !== folder.name) {
                folder.name = newName;
                saveData().then(() => updateFolderHeader(folderId));
            }
        });
    }

    async function deleteFolder(folderId) {
        Object.keys(conversationFolders).forEach(id => {
            if (conversationFolders[id] === folderId) delete conversationFolders[id];
        });
        folders = folders.filter(f => f.id !== folderId);
        await saveData();
        renderFolders();
    }

    function toggleFolder(folderId) {
        const folder = folders.find(f => f.id === folderId);
        if (folder) {
            folder.isClosed = !folder.isClosed;
            const folderEl = document.querySelector(`.folder[data-folder-id="${folderId}"]`);
            if (folderEl) folderEl.classList.toggle('closed');
            saveData();
        }
    }

    // --- CONTEXT MENU & DIALOGS ---
    function showContextMenu(event, folderId) {
        event.preventDefault();
        event.stopPropagation();
        closeContextMenu();

        const btn = event.currentTarget;
        const rect = btn.getBoundingClientRect();

        const menu = document.createElement('div');
        menu.className = 'folder-context-menu';
        menu.id = 'folder-context-menu-active';

        const items = {
            'Rename': () => renameFolder(folderId),
            'Change Color': () => showColorPickerDialog(folderId),
            'Delete Folder': () => showConfirmationDialog("Are you sure you want to delete this folder?", () => deleteFolder(folderId), "Delete", "dialog-btn-delete")
        };

        for (const [text, action] of Object.entries(items)) {
            const itemEl = document.createElement('div');
            itemEl.className = 'folder-context-menu-item';
            if (text === 'Delete Folder') itemEl.classList.add('delete');
            itemEl.textContent = text;
            itemEl.onclick = (e) => {
                e.stopPropagation();
                closeContextMenu();
                action(e);
            };
            menu.appendChild(itemEl);
        }

        document.body.appendChild(menu);
        menu.style.display = 'block';
        menu.style.top = `${rect.bottom + window.scrollY}px`;
        menu.style.left = `${rect.right + window.scrollX - menu.offsetWidth}px`;

        setTimeout(() => document.addEventListener('click', closeContextMenu, { once: true }), 0);
    }

    function closeContextMenu() {
        const menu = document.getElementById('folder-context-menu-active');
        if (menu) menu.remove();
    }

    function showColorPickerDialog(folderId) {
        const folder = folders.find(f => f.id === folderId);
        if (!folder) return;

        const overlay = document.createElement('div');
        overlay.className = 'custom-dialog-overlay';
        const dialogBox = document.createElement('div');
        dialogBox.className = 'custom-dialog-box color-picker-dialog';
        const titleH2 = document.createElement('h2');
        titleH2.textContent = 'Change Folder Color';
        const grid = document.createElement('div');
        grid.className = 'color-picker-grid';

        let selectedColor = folder.color;

        FOLDER_COLORS.forEach(color => {
            const swatch = document.createElement('div');
            swatch.className = 'color-swatch';
            if (color.toLowerCase() === selectedColor.toLowerCase()) swatch.classList.add('selected');
            swatch.style.backgroundColor = color;
            swatch.onclick = () => {
                selectedColor = color;
                hexInput.value = color;
                grid.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
                swatch.classList.add('selected');
            };
            grid.appendChild(swatch);
        });

        const hexInput = document.createElement('input');
        hexInput.className = 'custom-dialog-input';
        hexInput.type = 'text';
        hexInput.placeholder = 'Or enter a hex value, e.g. #C0FFEE';
        hexInput.value = selectedColor;

        const btnYes = document.createElement('button');
        btnYes.className = 'custom-dialog-btn dialog-btn-confirm';
        btnYes.textContent = 'Save';
        const btnNo = document.createElement('button');
        btnNo.className = 'custom-dialog-btn dialog-btn-cancel';
        btnNo.textContent = 'Cancel';

        dialogBox.appendChild(titleH2);
        dialogBox.appendChild(grid);
        dialogBox.appendChild(hexInput);
        dialogBox.appendChild(btnYes);
        dialogBox.appendChild(btnNo);
        overlay.appendChild(dialogBox);
        document.body.appendChild(overlay);

        btnYes.onclick = () => {
            const newColor = hexInput.value.trim();
            if (/^#[0-9A-F]{6}$/i.test(newColor)) {
                folder.color = newColor;
                saveData().then(() => updateFolderHeader(folderId));
                overlay.remove();
            } else {
                hexInput.style.border = "1px solid red";
                hexInput.value = "Invalid Hex Code";
                setTimeout(() => {
                    hexInput.style.border = "";
                    hexInput.value = selectedColor;
                }, 2000);
            }
        };
        btnNo.onclick = () => { overlay.remove(); };
    }

    function showConfirmationDialog(message, onConfirm, confirmText = "Confirm", confirmClass = "dialog-btn-confirm") {
        const overlay = document.createElement('div');
        overlay.className = 'custom-dialog-overlay';
        const dialogBox = document.createElement('div');
        dialogBox.className = 'custom-dialog-box';
        const messageP = document.createElement('p');
        messageP.textContent = message;
        const btnYes = document.createElement('button');
        btnYes.className = `custom-dialog-btn ${confirmClass}`;
        btnYes.textContent = confirmText;
        const btnNo = document.createElement('button');
        btnNo.className = 'custom-dialog-btn dialog-btn-cancel';
        btnNo.textContent = 'Cancel';
        dialogBox.appendChild(messageP);
        dialogBox.appendChild(btnYes);
        dialogBox.appendChild(btnNo);
        overlay.appendChild(dialogBox);
        document.body.appendChild(overlay);
        btnYes.onclick = () => { onConfirm(); overlay.remove(); };
        btnNo.onclick = () => { overlay.remove(); };
    }

    function showCustomPromptDialog(title, defaultValue, confirmText, onConfirm) {
        const overlay = document.createElement('div');
        overlay.className = 'custom-dialog-overlay';
        const dialogBox = document.createElement('div');
        dialogBox.className = 'custom-dialog-box';
        const titleH2 = document.createElement('h2');
        titleH2.textContent = title;
        const input = document.createElement('input');
        input.className = 'custom-dialog-input';
        input.type = 'text';
        input.value = defaultValue;
        const btnYes = document.createElement('button');
        btnYes.className = 'custom-dialog-btn dialog-btn-confirm';
        btnYes.textContent = confirmText;
        const btnNo = document.createElement('button');
        btnNo.className = 'custom-dialog-btn dialog-btn-cancel';
        btnNo.textContent = 'Cancel';
        dialogBox.appendChild(titleH2);
        dialogBox.appendChild(input);
        dialogBox.appendChild(btnYes);
        dialogBox.appendChild(btnNo);
        overlay.appendChild(dialogBox);
        document.body.appendChild(overlay);
        input.focus();
        input.select();
        btnYes.onclick = () => { onConfirm(input.value); overlay.remove(); };
        btnNo.onclick = () => { overlay.remove(); };
        input.onkeydown = (e) => { if (e.key === 'Enter') btnYes.click(); };
    }

    // --- DRAG AND DROP ---
    function setupDragAndDrop() {
        const chatListContainer = document.querySelector(CHAT_LIST_CONTAINER_SELECTOR);
        if (!chatListContainer) return;

        new Sortable(chatListContainer, {
            group: 'shared',
            animation: 150,
            onEnd: function() {
                rebuildAndSaveState();
            },
        });

        document.querySelectorAll('.folder-content').forEach(folderContentEl => {
            new Sortable(folderContentEl, {
                group: 'shared',
                animation: 150,
                onEnd: function() {
                    rebuildAndSaveState();
                },
            });
        });
    }

    function rebuildAndSaveState() {
        const newConversationFolders = {};
        document.querySelectorAll('.folder').forEach(folderEl => {
            const folderId = folderEl.dataset.folderId;
            folderEl.querySelectorAll(CHAT_CONTAINER_SELECTOR).forEach(item => {
                const id = getIdentifierFromElement(item.querySelector(CHAT_ITEM_SELECTOR));
                if (id) {
                    newConversationFolders[id] = folderId;
                }
            });
        });
        conversationFolders = newConversationFolders;
        saveData();
    }

    // --- INITIALIZATION ---
    function initialize() {
        const injectionPoint = document.querySelector(INJECTION_POINT_SELECTOR);
        if (!injectionPoint) return false;

        if (chatItemCache.size === 0) {
            const chats = document.querySelectorAll(CHAT_CONTAINER_SELECTOR);
            if (chats.length > 0) {
                chats.forEach(chat => {
                    const id = getIdentifierFromElement(chat.querySelector(CHAT_ITEM_SELECTOR));
                    if (id) chatItemCache.set(id, chat);
                });
            }
        }

        if (document.getElementById('folder-ui-container')) {
            organizeConversations();
            return true;
        }

        const uiContainer = document.createElement('div');
        uiContainer.id = 'folder-ui-container';
        const addButton = document.createElement('button');
        addButton.id = 'add-folder-btn';
        addButton.textContent = '+ New Folder';
        addButton.onclick = createNewFolder;
        const folderContainer = document.createElement('div');
        folderContainer.id = 'folder-container';
        uiContainer.appendChild(addButton);
        uiContainer.appendChild(folderContainer);
        injectionPoint.prepend(uiContainer);
        renderFolders();
        return true;
    }

    // --- MAIN EXECUTION ---
    loadData().then(() => {
        const initInterval = setInterval(() => {
            if (initialize()) {
                clearInterval(initInterval);
            }
        }, 500);
    });

    // --- OPTIONAL RESET BUTTON ---
    function addResetButton() {
        if (document.getElementById('reset-data-btn')) return;
        const resetButton = document.createElement('button');
        resetButton.id = 'reset-data-btn';
        resetButton.textContent = 'Reset Folder Data';
        resetButton.onclick = () => {
            showConfirmationDialog('Are you sure you want to delete all folder data? This cannot be undone.', () => {
                GM_deleteValue('gemini_folders');
                GM_deleteValue('gemini_convo_folders');
                location.reload();
            }, 'Reset', 'dialog-btn-delete');
        };
        document.body.appendChild(resetButton);
    }
    addResetButton();
    // --- END OPTIONAL RESET BUTTON ---
/******************************************************************
 *  DEBUG INSTRUMENTATION – add right above the final “})();”
 ******************************************************************/

// ---- Clipboard helper (works in all modern browsers) ----------
async function copyTextToClipboard(text) {
    try {
        await navigator.clipboard.writeText(text);
        console.info('[Gemini Folder Debug] Copied to clipboard');
    } catch (err) {
        console.warn('[Gemini Folder Debug] Clipboard write failed:', err);
    }
}

// ---- Collect full runtime + DOM state -------------------------
function collectDebugState() {
    const stateSnapshot = {
        storedFolders: folders,                       // what’s in memory
        storedConversationFolders: conversationFolders,
        domFolders: {},                               // what’s actually in the DOM
        domOrphans: []                                // chats not in any folder
    };

    // Map DOM placements
    document.querySelectorAll('.folder').forEach(folderEl => {
        const folderId = folderEl.dataset.folderId;
        stateSnapshot.domFolders[folderId] = [];
        folderEl.querySelectorAll('div[data-test-id="conversation"]').forEach(chatEl => {
            const id = getIdentifierFromElement(chatEl);
            if (id) stateSnapshot.domFolders[folderId].push(id);
        });
    });

    // Chats still living in the main container
    const mainContainer = document.querySelector(CHAT_LIST_CONTAINER_SELECTOR);
    if (mainContainer) {
        mainContainer.querySelectorAll('div[data-test-id="conversation"]').forEach(chatEl => {
            const id = getIdentifierFromElement(chatEl);
            if (id) stateSnapshot.domOrphans.push(id);
        });
    }

    return stateSnapshot;
}

// ---- Add floating button to UI --------------------------------
function addDebugButton() {
    if (document.getElementById('gemini-folder-debug-btn')) return;

    const btn = document.createElement('button');
    btn.id = 'gemini-folder-debug-btn';
    btn.textContent = '🩺 Copy Debug Info';
    Object.assign(btn.style, {
        position: 'fixed',
        bottom: '15px',
        left:  '15px',
        zIndex:  10000,
        padding: '8px 12px',
        fontSize: '13px',
        background: '#d32f2f',
        color: '#fff',
        border: '2px solid #fff',
        borderRadius: '6px',
        cursor: 'pointer'
    });

    btn.onclick = async () => {
        const snapshot = collectDebugState();
        const json = JSON.stringify(snapshot, null, 2);
        console.log('%c[Gemini Folder Debug] Snapshot below:', 'color:#d32f2f;font-weight:bold;');
        console.log(json);
        await copyTextToClipboard(json);
        btn.textContent = '✅ Copied!';
        setTimeout(() => { btn.textContent = '🩺 Copy Debug Info'; }, 2000);
    };

    document.body.appendChild(btn);
}

// run once UI is present
const debugInitInterval = setInterval(() => {
    if (document.querySelector(INJECTION_POINT_SELECTOR)) {
        addDebugButton();
        clearInterval(debugInitInterval);
    }
}, 600);

/******************************************************************
 *  END DEBUG INSTRUMENTATION
 ******************************************************************/
    /******************************************************************
 *  Header row (Folders  +  ⚙︎) – added without disturbing core  *
 ******************************************************************/

(function addHeaderOnce () {
  // Wait until the original UI container exists
  const check = setInterval(() => {
    const ui  = document.querySelector('#folder-ui-container');
    const add = ui?.querySelector('button, input[value="+ New Folder"]'); // old add-folder button
    if (!ui || !add) return;

    clearInterval(check);

    /* Hide the old UI bits ------------------------------------ */
    add.style.display = 'none';                    // old “+ New Folder” button
    document.querySelectorAll('.debug-btn, .reset-btn')
            .forEach(btn => btn.style.display = 'none'); // bottom red buttons

    /* Build header row --------------------------------------- */
    const header = document.createElement('div');
    header.id = 'folders-header-inline';
    header.style.cssText = 'display:flex;align-items:center;margin:8px 0 12px;font-family:Roboto,Arial,sans-serif;';

    const title = Object.assign(document.createElement('span'), { textContent: 'Folders', style: 'font-weight:600;' });
    const spacer = Object.assign(document.createElement('span'), { style: 'flex:1;' });

    const btnCSS = 'background:none;border:none;font-size:18px;cursor:pointer;width:28px;height:28px;border-radius:4px;display:flex;align-items:center;justify-content:center;';
    const plus  = Object.assign(document.createElement('button'), { textContent:'+', title:'New Folder', style:btnCSS });
    const gear  = Object.assign(document.createElement('button'), { textContent:'⚙︎', title:'Tools',      style:btnCSS });

    /* Re-use the existing functions already in the page */
    plus.onclick = () => add.click();                 // create-folder dialog
    gear.onclick = showToolsPop;                      // small pop-over

    header.append(title, spacer, plus, gear);
    ui.prepend(header);

    /* ---------- tiny pop-over ---------- */
    function showToolsPop (e) {
      e.stopPropagation();
      const old = document.getElementById('folder-tools-pop-inline');
      if (old) return old.remove();

      const pop = document.createElement('div');
      pop.id = 'folder-tools-pop-inline';
      pop.style.cssText = 'position:absolute;z-index:10000;background:#333;color:#fff;border:1px solid #555;border-radius:8px;padding:6px;font:14px Roboto,Arial;';
      const { bottom, right } = e.currentTarget.getBoundingClientRect();
      pop.style.top  = `${bottom + window.scrollY}px`;
      pop.style.left = `${right  + window.scrollX - 150}px`;
      pop.style.minWidth = '150px';

      const item = (txt, fn) => {
        const b = Object.assign(document.createElement('button'), { textContent:txt });
        b.style.cssText = 'display:block;width:100%;background:none;border:none;color:#fff;padding:8px 14px;text-align:left;border-radius:6px;cursor:pointer;';
        b.onmouseover = () => b.style.background = '#555';
        b.onmouseout  = () => b.style.background = '';
        b.onclick = fn;
        pop.appendChild(b);
      };

      item('Copy Debug Code', async () => {
        if (window.collectDebugState && navigator.clipboard)
          await navigator.clipboard.writeText(JSON.stringify(window.collectDebugState(), null, 2));
        pop.remove();
      });

      item('Reset Folder Data', () => {
        if (confirm('Delete all folder data?')) {
          GM_deleteValue('gemini_folders');
          GM_deleteValue('gemini_convo_folders');
          location.reload();
        }
      });

      document.body.appendChild(pop);
      setTimeout(() => document.addEventListener('click', () => pop.remove(), { once:true }), 0);
    }
  }, 300);
})();

})();