您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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); })(); })();