Last active
September 6, 2026 08:17
-
-
Save nmc-eco/858ad096626f034faf4e1a6235a6bef1 to your computer and use it in GitHub Desktop.
Fold / unfold answers and code blocks on Perplexity AI (using Violent Monkey)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Perplexity - Fold Answers by Default | |
| // @version 1.2 | |
| // @description Folds/collapses all answer sections on Perplexity AI sessions by default with click-to-expand. | |
| // @author Gemini | |
| // @match https://www.perplexity.ai/* | |
| // @grant GM_addStyle | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // Inject styling for the toggle header and folded state | |
| const styles = ` | |
| /* Fold toggle bar styling */ | |
| .pplx-fold-toggle { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| width: 100%; | |
| padding: 8px 12px; | |
| margin-bottom: 6px; | |
| background: rgba(128, 128, 128, 0.08); | |
| border: 1px solid rgba(128, 128, 128, 0.18); | |
| border-radius: 8px; | |
| cursor: pointer; | |
| user-select: none; | |
| font-size: 13px; | |
| line-height: 1.4; | |
| color: inherit; | |
| box-sizing: border-box; | |
| transition: background-color 0.15s ease, border-color 0.15s ease; | |
| } | |
| .pplx-fold-toggle:hover { | |
| background: rgba(128, 128, 128, 0.15); | |
| border-color: rgba(128, 128, 128, 0.3); | |
| } | |
| .pplx-fold-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| min-width: 0; | |
| } | |
| .pplx-fold-chevron { | |
| flex-shrink: 0; | |
| width: 16px; | |
| height: 16px; | |
| transition: transform 0.2s ease; | |
| } | |
| /* Rotate chevron to indicate collapsed vs expanded */ | |
| .pplx-is-folded .pplx-fold-chevron { | |
| transform: rotate(-90deg); | |
| } | |
| .pplx-fold-tag { | |
| font-weight: 600; | |
| font-size: 12px; | |
| opacity: 0.9; | |
| flex-shrink: 0; | |
| } | |
| .pplx-fold-preview-text { | |
| opacity: 0.6; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| font-weight: 400; | |
| } | |
| .pplx-fold-badge-state { | |
| font-size: 11px; | |
| opacity: 0.5; | |
| flex-shrink: 0; | |
| margin-left: 8px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| /* Hide the entire answer contents and footer when folded */ | |
| [data-workflow-final-text].pplx-is-folded > *:not(.pplx-fold-toggle) { | |
| display: none !important; | |
| } | |
| /* Code block folding */ | |
| figure.pplx-code-folded > :not(figcaption) { | |
| display: none !important; | |
| } | |
| figure.pplx-code-folded figcaption { | |
| border-bottom: none !important; | |
| } | |
| figcaption.pplx-code-header { | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .pplx-code-arrow { | |
| display: inline-block; | |
| margin-right: 6px; | |
| font-size: 10px; | |
| transition: transform 0.2s ease; | |
| } | |
| figure.pplx-code-folded .pplx-code-arrow { | |
| transform: rotate(-90deg); | |
| } | |
| /* Global Fold / Unfold All Floating Button */ | |
| .pplx-global-fold-btn { | |
| position: fixed; | |
| bottom: 24px; | |
| right: 24px; | |
| z-index: 9999; | |
| padding: 8px 14px; | |
| background: rgba(30, 30, 30, 0.85); | |
| color: #fff; | |
| backdrop-filter: blur(8px); | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| border-radius: 20px; | |
| font-size: 12px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| box-shadow: 0 4px 12px rgba(0,0,0,0.25); | |
| transition: all 0.2s ease; | |
| user-select: none; | |
| } | |
| .pplx-global-fold-btn:hover { | |
| transform: translateY(-2px); | |
| background: rgba(50, 50, 50, 0.95); | |
| } | |
| /* ---- Prompt navigation pane ---- */ | |
| .pplx-nav-pane { | |
| position: fixed; | |
| top: 72px; bottom: 24px; left: 64px; | |
| z-index: 9998; width: 250px; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| background: rgba(128, 128, 128, 0.08); | |
| color: inherit; | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(128, 128, 128, 0.18); | |
| border-radius: 12px; | |
| box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18); | |
| font-size: 13px; | |
| transition: | |
| width 0.2s ease, | |
| left 0.15s ease, | |
| background 0.2s ease; | |
| } | |
| /* Folded/collapsed appearance */ | |
| .pplx-nav-pane.pplx-nav-collapsed { | |
| width: 34px; | |
| height: 34px; | |
| bottom: auto; | |
| background: rgba(253, 251, 250, 0.05); | |
| border-color: rgba(0, 0, 0, 0.12); | |
| } | |
| .pplx-nav-pane.pplx-nav-collapsed .pplx-nav-list, | |
| .pplx-nav-pane.pplx-nav-collapsed .pplx-nav-title, | |
| .pplx-nav-pane.pplx-nav-collapsed .pplx-nav-rebuild { | |
| display: none; | |
| } | |
| .pplx-nav-head { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 8px 10px; | |
| flex-shrink: 0; | |
| border-bottom: 1px solid rgba(0, 0, 0, 0.12); | |
| } | |
| .pplx-nav-title { | |
| font-weight: 600; | |
| font-size: 11px; | |
| letter-spacing: 0.5px; | |
| text-transform: uppercase; | |
| opacity: 0.65; | |
| flex: 1; | |
| } | |
| .pplx-nav-rebuild, | |
| .pplx-nav-toggle { | |
| background: transparent; | |
| border: 0; | |
| color: inherit; | |
| cursor: pointer; | |
| } | |
| .pplx-nav-rebuild { | |
| opacity: 0.55; | |
| font-size: 14px; | |
| line-height: 1; | |
| padding: 2px 4px; | |
| } | |
| .pplx-nav-rebuild:hover { | |
| opacity: 1; | |
| } | |
| .pplx-nav-toggle { | |
| opacity: 0.7; | |
| padding: 2px 4px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .pplx-nav-caret { | |
| transition: transform 0.2s ease; | |
| } | |
| .pplx-nav-collapsed .pplx-nav-caret { | |
| transform: rotate(180deg); | |
| } | |
| .pplx-nav-list { | |
| overflow-y: auto; | |
| padding: 6px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .pplx-nav-status { | |
| padding: 10px; | |
| opacity: 0.6; | |
| font-size: 12px; | |
| } | |
| .pplx-nav-item { | |
| display: flex; | |
| gap: 7px; | |
| align-items: baseline; | |
| text-align: left; | |
| background: transparent; | |
| border: 0; | |
| color: inherit; | |
| font: inherit; | |
| line-height: 1.35; | |
| padding: 6px 8px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| } | |
| .pplx-nav-item:hover { | |
| background: rgba(0, 0, 0, 0.06); | |
| } | |
| .pplx-nav-item.pplx-nav-active { | |
| background: rgba(0, 0, 0, 0.10); | |
| } | |
| .pplx-nav-idx { | |
| opacity: 0.45; | |
| font-size: 11px; | |
| min-width: 15px; | |
| flex-shrink: 0; | |
| } | |
| .pplx-nav-text { | |
| display: -webkit-box; | |
| -webkit-line-clamp: 2; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| [data-workflow-final-text], | |
| [class~="group/user-bubble"] { | |
| scroll-margin-top: 90px; | |
| } | |
| `; | |
| if (typeof GM_addStyle !== 'undefined') { | |
| GM_addStyle(styles); | |
| } else { | |
| const styleEl = document.createElement('style'); | |
| styleEl.textContent = styles; | |
| document.head.appendChild(styleEl); | |
| } | |
| function cleanSnippet(str) { | |
| return str ? str.replace(/\s+/g, ' ').trim() : ''; | |
| } | |
| function foldAnswer(container) { | |
| // If already attached, update preview if text streamed in later | |
| if (container.dataset.pplxFoldInit) { | |
| const previewEl = container.querySelector('.pplx-fold-preview-text'); | |
| if (previewEl && previewEl.dataset.empty === 'true') { | |
| const text = cleanSnippet(container.querySelector('[data-renderer="lm"]')?.textContent); | |
| if (text) { | |
| previewEl.textContent = `— ${text.slice(0, 90)}...`; | |
| previewEl.dataset.empty = 'false'; | |
| } | |
| } | |
| return; | |
| } | |
| const lmRenderer = container.querySelector('[data-renderer="lm"]'); | |
| const text = lmRenderer ? cleanSnippet(lmRenderer.textContent) : ''; | |
| // Mark as initialized & fold by default | |
| container.dataset.pplxFoldInit = 'true'; | |
| container.classList.add('pplx-is-folded'); | |
| const previewSnippet = text ? `— ${text.slice(0, 90)}...` : ''; | |
| // Create the interactive toggle bar | |
| const toggleBar = document.createElement('div'); | |
| toggleBar.className = 'pplx-fold-toggle'; | |
| toggleBar.innerHTML = ` | |
| <div class="pplx-fold-left"> | |
| <svg class="pplx-fold-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="6 9 12 15 18 9"></polyline> | |
| </svg> | |
| <span class="pplx-fold-tag">Answer</span> | |
| <span class="pplx-fold-preview-text" data-empty="${!text}">${previewSnippet}</span> | |
| </div> | |
| <span class="pplx-fold-badge-state">Folded</span> | |
| `; | |
| // Click handler to toggle open/closed | |
| toggleBar.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const isFolded = container.classList.toggle('pplx-is-folded'); | |
| const stateEl = toggleBar.querySelector('.pplx-fold-badge-state'); | |
| if (stateEl) { | |
| stateEl.textContent = isFolded ? 'Folded' : 'Expanded'; | |
| } | |
| }); | |
| // Insert at top of answer container | |
| container.prepend(toggleBar); | |
| } | |
| function foldCodeBlock(figure) { | |
| if (figure.dataset.pplxCodeFoldInit) return; | |
| const figcaption = figure.querySelector('figcaption'); | |
| if (!figcaption) return; | |
| figure.dataset.pplxCodeFoldInit = 'true'; | |
| figure.classList.add('pplx-code-folded'); // Folded by default | |
| figcaption.classList.add('pplx-code-header'); | |
| // Add a clickable toggle arrow before the language name | |
| const arrow = document.createElement('span'); | |
| arrow.className = 'pplx-code-arrow'; | |
| arrow.textContent = '▼'; | |
| figcaption.prepend(arrow); | |
| // Click header (excluding copy button) to toggle | |
| figcaption.addEventListener('click', (e) => { | |
| if (e.target.closest('button')) return; | |
| figure.classList.toggle('pplx-code-folded'); | |
| }); | |
| } | |
| /* ================= Prompt navigation pane ================= */ | |
| const PROMPT_SEL = '[class~="group/user-bubble"]'; | |
| const navIndex = []; // built once per thread, never reordered | |
| let navBuiltPath = null; | |
| let navBuilding = false; | |
| let navCollapsed = true; | |
| let navActiveKey = null; | |
| function promptText(bubble) { | |
| const el = bubble.querySelector('span[class*="whitespace-pre-line"]') || | |
| bubble.querySelector('.select-text'); | |
| return cleanSnippet(el ? el.textContent : bubble.textContent); | |
| } | |
| function promptKey(bubble) { | |
| const stamp = bubble.querySelector('span.text-tertiary'); | |
| const when = stamp ? cleanSnippet(stamp.textContent) : ''; | |
| return (when ? when + '|' : '') + promptText(bubble).slice(0, 120); | |
| } | |
| // Perplexity may scroll an inner container rather than the window | |
| function getScroller() { | |
| const probe = document.querySelector(PROMPT_SEL); | |
| let el = probe ? probe.parentElement : null; | |
| while (el && el !== document.body) { | |
| const oy = getComputedStyle(el).overflowY; | |
| if ((oy === 'auto' || oy === 'scroll') && el.scrollHeight > el.clientHeight + 40) return el; | |
| el = el.parentElement; | |
| } | |
| return document.scrollingElement || document.documentElement; | |
| } | |
| function isWindowScroller(sc) { | |
| return sc === document.scrollingElement || sc === document.documentElement || sc === document.body; | |
| } | |
| function scrollerTop(sc) { | |
| return isWindowScroller(sc) ? window.scrollY : sc.scrollTop; | |
| } | |
| function setScrollerTop(sc, v, smooth) { | |
| const behavior = smooth ? 'smooth' : 'auto'; | |
| if (isWindowScroller(sc)) window.scrollTo({ top: v, behavior }); | |
| else sc.scrollTo({ top: v, behavior }); | |
| } | |
| const wait = ms => new Promise(r => setTimeout(r, ms)); | |
| // Sweep the whole thread top-to-bottom once, recording prompts in document order | |
| async function buildIndex() { | |
| if (navBuilding) return; | |
| navBuilding = true; | |
| navBuiltPath = location.pathname; | |
| navIndex.length = 0; | |
| navActiveKey = null; | |
| renderNav('Scanning thread…'); | |
| const sc = getScroller(); | |
| const startTop = scrollerTop(sc); | |
| const seen = Object.create(null); | |
| const viewH = isWindowScroller(sc) ? window.innerHeight : sc.clientHeight; | |
| const step = Math.max(200, viewH * 0.6); | |
| setScrollerTop(sc, 0, false); | |
| await wait(250); | |
| let pos = 0, guard = 0; | |
| while (guard++ < 400) { | |
| document.querySelectorAll(PROMPT_SEL).forEach(bubble => { | |
| const key = promptKey(bubble); | |
| const text = promptText(bubble); | |
| if (!text || seen[key]) return; | |
| seen[key] = true; | |
| navIndex.push({ key, text, ratio: 0 }); | |
| }); | |
| const max = (isWindowScroller(sc) | |
| ? document.documentElement.scrollHeight - window.innerHeight | |
| : sc.scrollHeight - sc.clientHeight); | |
| if (pos >= max) break; | |
| pos = Math.min(pos + step, max); | |
| setScrollerTop(sc, pos, false); | |
| await wait(150); | |
| } | |
| // record a rough position for each, used only as a fallback when jumping | |
| navIndex.forEach((e, i) => { e.ratio = navIndex.length > 1 ? i / (navIndex.length - 1) : 0; }); | |
| setScrollerTop(sc, 0, false); | |
| await wait(120); | |
| setScrollerTop(sc, startTop === 0 ? 0 : 0, true); | |
| navBuilding = false; | |
| renderNav(); | |
| } | |
| async function jumpTo(entry) { | |
| let node = null; | |
| // exact key match (includes timestamp) | |
| document.querySelectorAll(PROMPT_SEL).forEach(b => { | |
| if (!node && promptKey(b) === entry.key) node = b; | |
| }); | |
| // fallback: text-only match (timestamps drift, keys go stale) | |
| if (!node) { | |
| document.querySelectorAll(PROMPT_SEL).forEach(b => { | |
| if (!node && promptText(b) === entry.text) node = b; | |
| }); | |
| } | |
| if (!node) { | |
| // temporarily fold everything so ratio-based scroll is accurate | |
| const wasExpanded = []; | |
| document.querySelectorAll('[data-workflow-final-text]:not(.pplx-is-folded)').forEach(a => { | |
| wasExpanded.push(a); | |
| a.classList.add('pplx-is-folded'); | |
| }); | |
| const sc = getScroller(); | |
| const max = (isWindowScroller(sc) | |
| ? document.documentElement.scrollHeight - window.innerHeight | |
| : sc.scrollHeight - sc.clientHeight); | |
| setScrollerTop(sc, max * entry.ratio, false); | |
| await wait(350); | |
| // restore what was open | |
| wasExpanded.forEach(a => a.classList.remove('pplx-is-folded')); | |
| document.querySelectorAll(PROMPT_SEL).forEach(b => { | |
| if (!node && promptText(b) === entry.text) node = b; | |
| }); | |
| if (!node) return; | |
| } | |
| // unfold the answer that follows this prompt, if there is one | |
| let sib = node.nextElementSibling, answer = null, hops = 0; | |
| while (sib && hops++ < 6 && !answer) { | |
| answer = sib.matches('[data-workflow-final-text]') | |
| ? sib : sib.querySelector('[data-workflow-final-text]'); | |
| sib = sib.nextElementSibling; | |
| } | |
| if (answer) { | |
| answer.classList.remove('pplx-is-folded'); | |
| const st = answer.querySelector('.pplx-fold-badge-state'); | |
| if (st) st.textContent = 'Expanded'; | |
| } | |
| node.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| function positionNavPane() { | |
| const pane = document.getElementById('pplx-nav-pane'); | |
| if (!pane) return; | |
| const anchor = document.querySelector('.w-sideBarWidth') || | |
| document.querySelector('nav[aria-label="Main"]'); | |
| pane.style.left = ((anchor ? anchor.getBoundingClientRect().right : 0) + 8) + 'px'; | |
| } | |
| function ensureNavPane() { | |
| let pane = document.getElementById('pplx-nav-pane'); | |
| if (pane) return pane; | |
| pane = document.createElement('div'); | |
| pane.id = 'pplx-nav-pane'; | |
| pane.className = 'pplx-nav-pane pplx-nav-collapsed'; | |
| pane.innerHTML = ` | |
| <div class="pplx-nav-head"> | |
| <button class="pplx-nav-toggle" title="Collapse"> | |
| <svg class="pplx-nav-caret" width="14" height="14" viewBox="0 0 24 24" fill="none" | |
| stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <polyline points="15 18 9 12 15 6"></polyline> | |
| </svg> | |
| </button> | |
| <span class="pplx-nav-title">Prompts</span> | |
| <button class="pplx-nav-rebuild" title="Rescan thread">⟳</button> | |
| </div> | |
| <div class="pplx-nav-list"></div> | |
| `; | |
| pane.querySelector('.pplx-nav-toggle').addEventListener('click', () => { | |
| navCollapsed = !navCollapsed; | |
| pane.classList.toggle('pplx-nav-collapsed', navCollapsed); | |
| }); | |
| pane.querySelector('.pplx-nav-rebuild').addEventListener('click', () => buildIndex()); | |
| document.body.appendChild(pane); | |
| positionNavPane(); | |
| window.addEventListener('resize', positionNavPane); | |
| setInterval(positionNavPane, 500); | |
| return pane; | |
| } | |
| function renderNav(statusMsg) { | |
| const pane = ensureNavPane(); | |
| const list = pane.querySelector('.pplx-nav-list'); | |
| list.textContent = ''; | |
| if (statusMsg) { | |
| const s = document.createElement('div'); | |
| s.className = 'pplx-nav-status'; | |
| s.textContent = statusMsg; | |
| list.appendChild(s); | |
| return; | |
| } | |
| if (!navIndex.length) { | |
| const s = document.createElement('div'); | |
| s.className = 'pplx-nav-status'; | |
| s.textContent = 'No prompts found.'; | |
| list.appendChild(s); | |
| return; | |
| } | |
| navIndex.forEach((entry, i) => { | |
| const item = document.createElement('button'); | |
| item.type = 'button'; | |
| item.className = 'pplx-nav-item' + (entry.key === navActiveKey ? ' pplx-nav-active' : ''); | |
| item.title = entry.text; | |
| item.innerHTML = `<span class="pplx-nav-idx">${i + 1}</span><span class="pplx-nav-text"></span>`; | |
| item.querySelector('.pplx-nav-text').textContent = entry.text; | |
| item.addEventListener('click', () => { | |
| navActiveKey = entry.key; | |
| list.querySelectorAll('.pplx-nav-active').forEach(el => el.classList.remove('pplx-nav-active')); | |
| item.classList.add('pplx-nav-active'); | |
| jumpTo(entry); | |
| }); | |
| list.appendChild(item); | |
| }); | |
| } | |
| // build once per thread; re-build when the URL changes | |
| let navBuildTimer = null; | |
| function maybeBuildIndex() { | |
| ensureNavPane(); | |
| if (navBuilding) return; | |
| if (navBuiltPath === location.pathname) return; | |
| if (!document.querySelector(PROMPT_SEL)) return; // thread not rendered yet | |
| if (navBuildTimer) return; // already waiting to start | |
| navBuildTimer = setTimeout(() => { | |
| navBuildTimer = null; | |
| if (navBuilding || navBuiltPath === location.pathname) return; | |
| buildIndex(); | |
| }, 1200); // let the thread settle first | |
| } | |
| const NAV_AUTO_APPEND = true; // set false to disable auto-adding new prompts | |
| // append prompts sent after the initial scan, without rescanning the thread | |
| function appendNewPrompts() { | |
| if (!NAV_AUTO_APPEND || navBuilding || navBuiltPath !== location.pathname) return; | |
| const known = new Set(navIndex.map(e => e.text)); | |
| const bubbles = Array.from(document.querySelectorAll(PROMPT_SEL)); | |
| // only look at bubbles that come after the last already-indexed one | |
| let start = 0; | |
| for (let i = bubbles.length - 1; i >= 0; i--) { | |
| if (known.has(promptText(bubbles[i]))) { start = i + 1; break; } | |
| } | |
| let added = false; | |
| for (let i = start; i < bubbles.length; i++) { | |
| const text = promptText(bubbles[i]); | |
| if (!text || known.has(text)) continue; | |
| navIndex.push({ key: promptKey(bubbles[i]), text, ratio: 1 }); | |
| known.add(text); | |
| added = true; | |
| } | |
| if (added) { | |
| navIndex.forEach((e, i) => { e.ratio = navIndex.length > 1 ? i / (navIndex.length - 1) : 0; }); | |
| renderNav(); | |
| } | |
| } | |
| let allFolded = true; | |
| function toggleAllAnswers(shouldFold) { | |
| const answers = document.querySelectorAll('[data-workflow-final-text]'); | |
| answers.forEach(container => { | |
| container.classList.toggle('pplx-is-folded', shouldFold); | |
| const stateEl = container.querySelector('.pplx-fold-badge-state'); | |
| if (stateEl) { | |
| stateEl.textContent = shouldFold ? 'Folded' : 'Expanded'; | |
| } | |
| }); | |
| } | |
| function ensureGlobalToggleBtn() { | |
| if (document.getElementById('pplx-global-toggle-btn')) return; | |
| const btn = document.createElement('button'); | |
| btn.id = 'pplx-global-toggle-btn'; | |
| btn.className = 'pplx-global-fold-btn'; | |
| btn.textContent = '▼ Unfold All'; | |
| btn.addEventListener('click', () => { | |
| allFolded = !allFolded; | |
| toggleAllAnswers(allFolded); | |
| btn.textContent = allFolded ? '▼ Unfold All' : '▲ Fold All'; | |
| }); | |
| document.body.appendChild(btn); | |
| } | |
| function scanAndFold() { | |
| ensureGlobalToggleBtn(); | |
| maybeBuildIndex(); | |
| appendNewPrompts(); | |
| const answers = document.querySelectorAll('[data-workflow-final-text]'); | |
| answers.forEach(container => foldAnswer(container)); | |
| document.querySelectorAll('pre figure').forEach(fig => foldCodeBlock(fig)); | |
| } | |
| // Run on initial load | |
| scanAndFold(); | |
| // Use MutationObserver to handle SPA page changes and dynamically loaded threads | |
| let scanQueued = false; | |
| const observer = new MutationObserver(() => { | |
| if (scanQueued) return; | |
| scanQueued = true; | |
| setTimeout(() => { scanQueued = false; scanAndFold(); }, 200); | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment