Last active
September 6, 2026 13:07
-
-
Save nmc-eco/2996f5b032e0cf32e184f749a6c2cb2b to your computer and use it in GitHub Desktop.
Folds all answer sections & code on Claude.ai sessions by default
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 Claude.ai - Fold Answers by Default | |
| // @version 1.0 | |
| // @description Folds/collapses all answer sections on Claude.ai sessions by default with click-to-expand. Includes code-block folding and a prompt navigation pane. | |
| // @author Adapted from Perplexity-Fold v1.2 | |
| // @match https://claude.ai/* | |
| // @grant GM_addStyle | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| /* ================================================================ | |
| SELECTORS — claude.ai DOM structure | |
| ================================================================ | |
| Assistant row : div[data-testid="transcript-row"][data-perf-row="assistant"] | |
| User row : div[data-testid="transcript-row"][data-perf-row="human"] | |
| Reply text : [data-perf-reply-text] | |
| Code group : div[role="group"][aria-label$=" code"] | |
| Language label: child div with class containing "text-text-500" | |
| User msg text : [data-testid="user-message"] p | |
| Timestamp : time[data-cds="RelativeTime"] | |
| ================================================================ */ | |
| const ANSWER_SEL = 'div[data-testid="transcript-row"][data-perf-row="assistant"]'; | |
| const PROMPT_SEL = 'div[data-testid="transcript-row"][data-perf-row="human"]'; | |
| const CODE_SEL = 'div[role="group"][aria-label$=" code"]'; | |
| // ── Inject CSS ────────────────────────────────────────────────── | |
| const styles = ` | |
| /* ── Answer fold toggle bar ───────────────────────────── */ | |
| .cai-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; | |
| } | |
| .cai-fold-toggle:hover { | |
| background: rgba(128, 128, 128, 0.15); | |
| border-color: rgba(128, 128, 128, 0.3); | |
| } | |
| .cai-fold-left { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| min-width: 0; | |
| } | |
| .cai-fold-chevron { | |
| flex-shrink: 0; | |
| width: 16px; | |
| height: 16px; | |
| transition: transform 0.2s ease; | |
| } | |
| .cai-is-folded .cai-fold-chevron { | |
| transform: rotate(-90deg); | |
| } | |
| .cai-fold-tag { | |
| font-weight: 600; | |
| font-size: 12px; | |
| opacity: 0.9; | |
| flex-shrink: 0; | |
| } | |
| .cai-fold-preview-text { | |
| opacity: 0.6; | |
| overflow: hidden; | |
| white-space: nowrap; | |
| text-overflow: ellipsis; | |
| font-weight: 400; | |
| } | |
| .cai-fold-badge-state { | |
| font-size: 11px; | |
| opacity: 0.5; | |
| flex-shrink: 0; | |
| margin-left: 8px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| /* Hide everything inside a folded answer except the toggle */ | |
| ${ANSWER_SEL}.cai-is-folded > *:not(.cai-fold-toggle) { | |
| display: none !important; | |
| } | |
| /* ── Code-block folding ───────────────────────────────── */ | |
| div[role="group"].cai-code-folded > *:not(.cai-code-header) { | |
| display: none !important; | |
| } | |
| .cai-code-header { | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| .cai-code-arrow { | |
| display: inline-block; | |
| margin-right: 6px; | |
| font-size: 10px; | |
| transition: transform 0.2s ease; | |
| } | |
| div[role="group"].cai-code-folded .cai-code-arrow { | |
| transform: rotate(-90deg); | |
| } | |
| /* ── Global Fold / Unfold All button ──────────────────── */ | |
| .cai-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; | |
| } | |
| .cai-global-fold-btn:hover { | |
| transform: translateY(-2px); | |
| background: rgba(50, 50, 50, 0.95); | |
| } | |
| /* ── Prompt navigation pane ───────────────────────────── */ | |
| .cai-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; | |
| } | |
| .cai-nav-pane.cai-nav-collapsed { | |
| width: 34px; | |
| height: 34px; | |
| bottom: auto; | |
| background: rgba(253, 251, 250, 0.05); | |
| border-color: rgba(0, 0, 0, 0.12); | |
| } | |
| .cai-nav-pane.cai-nav-collapsed .cai-nav-list, | |
| .cai-nav-pane.cai-nav-collapsed .cai-nav-title, | |
| .cai-nav-pane.cai-nav-collapsed .cai-nav-rebuild { | |
| display: none; | |
| } | |
| .cai-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); | |
| } | |
| .cai-nav-title { | |
| font-weight: 600; | |
| font-size: 11px; | |
| letter-spacing: 0.5px; | |
| text-transform: uppercase; | |
| opacity: 0.65; | |
| flex: 1; | |
| } | |
| .cai-nav-rebuild, | |
| .cai-nav-toggle { | |
| background: transparent; | |
| border: 0; | |
| color: inherit; | |
| cursor: pointer; | |
| } | |
| .cai-nav-rebuild { | |
| opacity: 0.55; | |
| font-size: 14px; | |
| line-height: 1; | |
| padding: 2px 4px; | |
| } | |
| .cai-nav-rebuild:hover { opacity: 1; } | |
| .cai-nav-toggle { | |
| opacity: 0.7; | |
| padding: 2px 4px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .cai-nav-caret { | |
| transition: transform 0.2s ease; | |
| } | |
| .cai-nav-collapsed .cai-nav-caret { | |
| transform: rotate(180deg); | |
| } | |
| .cai-nav-list { | |
| overflow-y: auto; | |
| padding: 6px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .cai-nav-status { | |
| padding: 10px; | |
| opacity: 0.6; | |
| font-size: 12px; | |
| } | |
| .cai-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; | |
| } | |
| .cai-nav-item:hover { | |
| background: rgba(0, 0, 0, 0.06); | |
| } | |
| .cai-nav-item.cai-nav-active { | |
| background: rgba(0, 0, 0, 0.10); | |
| } | |
| .cai-nav-idx { | |
| opacity: 0.45; | |
| font-size: 11px; | |
| min-width: 15px; | |
| flex-shrink: 0; | |
| } | |
| .cai-nav-text { | |
| display: -webkit-box; | |
| -webkit-line-clamp: 2; | |
| -webkit-box-orient: vertical; | |
| overflow: hidden; | |
| } | |
| /* Smooth-scroll offset for header clearance */ | |
| div[data-testid="transcript-row"] { | |
| 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); | |
| } | |
| // ── Helpers ────────────────────────────────────────────────────── | |
| function cleanSnippet(str) { | |
| return str ? str.replace(/\s+/g, ' ').trim() : ''; | |
| } | |
| // ── Answer folding ────────────────────────────────────────────── | |
| function answerPreview(row) { | |
| // Primary: the rendered reply text | |
| const reply = row.querySelector('[data-perf-reply-text]'); | |
| if (reply) return cleanSnippet(reply.textContent); | |
| // Fallback: the sr-only heading "Claude responded: …" | |
| const h2 = row.querySelector('h2.sr-only'); | |
| if (h2) return cleanSnippet(h2.textContent.replace(/^Claude responded:\s*/i, '')); | |
| return ''; | |
| } | |
| function foldAnswer(container) { | |
| // Skip messages that are still streaming | |
| if (container.dataset.perfRowStreaming === 'true') return; | |
| // If already attached, update preview if text streamed in later | |
| if (container.dataset.caiFoldInit) { | |
| const previewEl = container.querySelector('.cai-fold-preview-text'); | |
| if (previewEl && previewEl.dataset.empty === 'true') { | |
| const text = answerPreview(container); | |
| if (text) { | |
| previewEl.textContent = '\u2014 ' + text.slice(0, 90) + '\u2026'; | |
| previewEl.dataset.empty = 'false'; | |
| } | |
| } | |
| return; | |
| } | |
| const text = answerPreview(container); | |
| // Mark as initialized and fold by default | |
| container.dataset.caiFoldInit = 'true'; | |
| // container.classList.add('cai-is-folded'); | |
| const previewSnippet = text | |
| ? '\u2014 ' + text.slice(0, 90) + '\u2026' | |
| : ''; | |
| // Build the interactive toggle bar | |
| const toggleBar = document.createElement('div'); | |
| toggleBar.className = 'cai-fold-toggle'; | |
| toggleBar.innerHTML = ` | |
| <div class="cai-fold-left"> | |
| <svg class="cai-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="cai-fold-tag">Answer</span> | |
| <span class="cai-fold-preview-text" data-empty="${!text}">${previewSnippet}</span> | |
| </div> | |
| <span class="cai-fold-badge-state">Folded</span> | |
| `; | |
| toggleBar.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| const isFolded = container.classList.toggle('cai-is-folded'); | |
| const badge = toggleBar.querySelector('.cai-fold-badge-state'); | |
| if (badge) badge.textContent = isFolded ? 'Folded' : 'Expanded'; | |
| }); | |
| container.prepend(toggleBar); | |
| } | |
| // ── Code-block folding ────────────────────────────────────────── | |
| function foldCodeBlock(codeGroup) { | |
| if (codeGroup.dataset.caiCodeFoldInit) return; | |
| // Find the language-label div. | |
| // The aria-label reads e.g. "python code"; the child div shows "python". | |
| const ariaLabel = codeGroup.getAttribute('aria-label') || ''; | |
| const lang = ariaLabel.replace(/\s*code$/i, '').trim().toLowerCase(); | |
| let langDiv = null; | |
| if (lang) { | |
| // Match by text content | |
| langDiv = Array.from(codeGroup.children).find(c => | |
| cleanSnippet(c.textContent).toLowerCase() === lang | |
| ); | |
| } | |
| // Fallback: look for the Tailwind token class | |
| if (!langDiv) { | |
| langDiv = codeGroup.querySelector(':scope > [class*="text-text-500"]'); | |
| } | |
| if (!langDiv) return; // cannot find header — skip | |
| codeGroup.dataset.caiCodeFoldInit = 'true'; | |
| codeGroup.classList.add('cai-code-folded'); // folded by default | |
| langDiv.classList.add('cai-code-header'); | |
| // Prepend a toggle arrow | |
| const arrow = document.createElement('span'); | |
| arrow.className = 'cai-code-arrow'; | |
| arrow.textContent = '\u25BC'; // ▼ | |
| langDiv.prepend(arrow); | |
| // Click header to toggle (but skip if the click hit the copy button) | |
| langDiv.addEventListener('click', (e) => { | |
| if (e.target.closest('button')) return; | |
| codeGroup.classList.toggle('cai-code-folded'); | |
| }); | |
| } | |
| // ── Prompt navigation pane ────────────────────────────────────── | |
| const navIndex = []; | |
| let navBuiltPath = null; | |
| let navBuilding = false; | |
| let navCollapsed = true; | |
| let navActiveKey = null; | |
| const NAV_AUTO_APPEND = true; | |
| function promptText(row) { | |
| // Primary: the <p> inside the user-message block | |
| const p = row.querySelector('[data-testid="user-message"] p'); | |
| if (p) return cleanSnippet(p.textContent); | |
| // Fallback: sr-only heading "You said: …" | |
| const h2 = row.querySelector('h2.sr-only'); | |
| if (h2) return cleanSnippet(h2.textContent.replace(/^You said:\s*/i, '')); | |
| return ''; | |
| } | |
| function promptKey(row) { | |
| const stamp = row.querySelector('time[data-cds="RelativeTime"]'); | |
| const when = stamp ? cleanSnippet(stamp.textContent) : ''; | |
| return (when ? when + '|' : '') + promptText(row).slice(0, 120); | |
| } | |
| // claude.ai scrolls an inner container, not the window | |
| function getScroller() { | |
| const probe = document.querySelector(PROMPT_SEL) || | |
| document.querySelector(ANSWER_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 entire thread top-to-bottom once, recording prompts in order | |
| async function buildIndex() { | |
| if (navBuilding) return; | |
| navBuilding = true; | |
| navBuiltPath = location.pathname; | |
| navIndex.length = 0; | |
| navActiveKey = null; | |
| renderNav('Scanning thread\u2026'); | |
| 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(row => { | |
| const key = promptKey(row); | |
| const text = promptText(row); | |
| 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); | |
| } | |
| 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(r => { | |
| if (!node && promptKey(r) === entry.key) node = r; | |
| }); | |
| // Fallback: text-only match | |
| if (!node) { | |
| document.querySelectorAll(PROMPT_SEL).forEach(r => { | |
| if (!node && promptText(r) === entry.text) node = r; | |
| }); | |
| } | |
| if (!node) { | |
| // Temporarily fold all answers so ratio-based scroll is accurate | |
| const wasExpanded = []; | |
| document.querySelectorAll(ANSWER_SEL + ':not(.cai-is-folded)').forEach(a => { | |
| wasExpanded.push(a); | |
| a.classList.add('cai-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); | |
| wasExpanded.forEach(a => a.classList.remove('cai-is-folded')); | |
| document.querySelectorAll(PROMPT_SEL).forEach(r => { | |
| if (!node && promptText(r) === entry.text) node = r; | |
| }); | |
| if (!node) return; | |
| } | |
| // Unfold the answer that follows this prompt | |
| let sib = node.nextElementSibling, answer = null, hops = 0; | |
| while (sib && hops++ < 6 && !answer) { | |
| if (sib.matches(ANSWER_SEL)) answer = sib; | |
| sib = sib.nextElementSibling; | |
| } | |
| if (answer) { | |
| answer.classList.remove('cai-is-folded'); | |
| const badge = answer.querySelector('.cai-fold-badge-state'); | |
| if (badge) badge.textContent = 'Expanded'; | |
| } | |
| node.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| // ── Nav-pane positioning ──────────────────────────────────────── | |
| function positionNavPane() { | |
| const pane = document.getElementById('cai-nav-pane'); | |
| if (!pane) return; | |
| // The claude.ai sidebar is <aside class="dframe-sidebar">. | |
| // When visible its right edge is > 0; when hidden it collapses to 0. | |
| const sidebar = document.querySelector('aside.dframe-sidebar'); | |
| if (sidebar) { | |
| const right = sidebar.getBoundingClientRect().right; | |
| pane.style.left = (right > 8 ? right + 8 : 8) + 'px'; | |
| } else { | |
| pane.style.left = '8px'; | |
| } | |
| } | |
| function ensureNavPane() { | |
| let pane = document.getElementById('cai-nav-pane'); | |
| if (pane) return pane; | |
| pane = document.createElement('div'); | |
| pane.id = 'cai-nav-pane'; | |
| pane.className = 'cai-nav-pane cai-nav-collapsed'; | |
| pane.innerHTML = ` | |
| <div class="cai-nav-head"> | |
| <button class="cai-nav-toggle" title="Collapse"> | |
| <svg class="cai-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="cai-nav-title">Prompts</span> | |
| <button class="cai-nav-rebuild" title="Rescan thread">\u27F3</button> | |
| </div> | |
| <div class="cai-nav-list"></div> | |
| `; | |
| pane.querySelector('.cai-nav-toggle').addEventListener('click', () => { | |
| navCollapsed = !navCollapsed; | |
| pane.classList.toggle('cai-nav-collapsed', navCollapsed); | |
| }); | |
| pane.querySelector('.cai-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('.cai-nav-list'); | |
| list.textContent = ''; | |
| if (statusMsg) { | |
| const s = document.createElement('div'); | |
| s.className = 'cai-nav-status'; | |
| s.textContent = statusMsg; | |
| list.appendChild(s); | |
| return; | |
| } | |
| if (!navIndex.length) { | |
| const s = document.createElement('div'); | |
| s.className = 'cai-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 = 'cai-nav-item' + (entry.key === navActiveKey ? ' cai-nav-active' : ''); | |
| item.title = entry.text; | |
| item.innerHTML = `<span class="cai-nav-idx">${i + 1}</span><span class="cai-nav-text"></span>`; | |
| item.querySelector('.cai-nav-text').textContent = entry.text; | |
| item.addEventListener('click', () => { | |
| navActiveKey = entry.key; | |
| list.querySelectorAll('.cai-nav-active').forEach(el => el.classList.remove('cai-nav-active')); | |
| item.classList.add('cai-nav-active'); | |
| jumpTo(entry); | |
| }); | |
| list.appendChild(item); | |
| }); | |
| } | |
| // ── Index build triggers ──────────────────────────────────────── | |
| let navBuildTimer = null; | |
| function maybeBuildIndex() { | |
| ensureNavPane(); | |
| if (navBuilding) return; | |
| if (navBuiltPath === location.pathname) return; | |
| if (!document.querySelector(PROMPT_SEL)) return; | |
| if (navBuildTimer) return; | |
| navBuildTimer = setTimeout(() => { | |
| navBuildTimer = null; | |
| if (navBuilding || navBuiltPath === location.pathname) return; | |
| buildIndex(); | |
| }, 1200); | |
| } | |
| // Append prompts sent after the initial scan — no rescan required | |
| 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)); | |
| 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, idx) => { | |
| e.ratio = navIndex.length > 1 ? idx / (navIndex.length - 1) : 0; | |
| }); | |
| renderNav(); | |
| } | |
| } | |
| // ── Global fold / unfold all ──────────────────────────────────── | |
| let allFolded = false; | |
| function toggleAllAnswers(shouldFold) { | |
| document.querySelectorAll(ANSWER_SEL).forEach(container => { | |
| container.classList.toggle('cai-is-folded', shouldFold); | |
| const badge = container.querySelector('.cai-fold-badge-state'); | |
| if (badge) badge.textContent = shouldFold ? 'Folded' : 'Expanded'; | |
| }); | |
| } | |
| function ensureGlobalToggleBtn() { | |
| if (document.getElementById('cai-global-toggle-btn')) return; | |
| const btn = document.createElement('button'); | |
| btn.id = 'cai-global-toggle-btn'; | |
| btn.className = 'cai-global-fold-btn'; | |
| btn.textContent = '\u25B2 Fold All'; | |
| btn.addEventListener('click', () => { | |
| allFolded = !allFolded; | |
| toggleAllAnswers(allFolded); | |
| btn.textContent = allFolded ? '\u25BC Unfold All' : '\u25B2 Fold All'; | |
| }); | |
| document.body.appendChild(btn); | |
| } | |
| // ── Main scan loop ────────────────────────────────────────────── | |
| function scanAndFold() { | |
| ensureGlobalToggleBtn(); | |
| maybeBuildIndex(); | |
| appendNewPrompts(); | |
| // Fold answer rows | |
| document.querySelectorAll(ANSWER_SEL).forEach(row => foldAnswer(row)); | |
| // Fold code blocks | |
| document.querySelectorAll(CODE_SEL).forEach(grp => foldCodeBlock(grp)); | |
| } | |
| // Initial run | |
| scanAndFold(); | |
| // MutationObserver for SPA navigation and streamed content | |
| 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