Skip to content

Instantly share code, notes, and snippets.

@nmc-eco
Last active September 6, 2026 13:07
Show Gist options
  • Select an option

  • Save nmc-eco/2996f5b032e0cf32e184f749a6c2cb2b to your computer and use it in GitHub Desktop.

Select an option

Save nmc-eco/2996f5b032e0cf32e184f749a6c2cb2b to your computer and use it in GitHub Desktop.
Folds all answer sections & code on Claude.ai sessions by default
// ==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