// ==UserScript==
// @name Collapse ChatGPT Code Blocks
// @namespace https://github.com/you/collapse-chatgpt-code
// @version 1.0.0
// @description Collapse/expand user & assistant code blocks on chat.openai.com/chatgpt.com using stable DOM attributes instead of brittle classnames.
// @author Chef D
// @match https://chat.openai.com/*
// @match https://chatgpt.com/*
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// ---------------------------
// SVGs (inline, no external deps)
// ---------------------------
const TOGGLE_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" style="transition: transform 120ms ease;">
<path d="M8.12 9.29L12 13.17l3.88-3.88a1 1 0 011.41 1.41l-4.59 4.59a1 1 0 01-1.41 0L6.71 10.7a1 1 0 111.41-1.41z" fill="currentColor"/>
</svg>`.trim();
const COPY_SVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24">
<path d="M16 1H4a2 2 0 00-2 2v12h2V3h12V1zm3 4H8a2 2 0 00-2 2v14a2 2 0 002 2h11a2 2 0 002-2V7a2 2 0 00-2-2zm0 16H8V7h11v14z" fill="currentColor"/>
</svg>`.trim();
// ---------------------------
// Utilities
// ---------------------------
const debounce = (fn, wait = 150) => {
let t;
return (...args) => {
clearTimeout(t);
t = setTimeout(() => fn(...args), wait);
};
};
function createBtn({ label, onClick, title, style = '' }) {
const btn = document.createElement('button');
btn.type = 'button';
btn.title = title || label;
btn.textContent = label;
btn.style.cssText = [
'font: 12px/1 system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif;',
'padding: 4px 8px;',
'border-radius: 6px;',
'border: 1px solid rgba(0,0,0,0.2);',
'background: rgba(240,240,240,0.9);',
'color: #111;',
'cursor: pointer;',
'user-select: none;',
'display: inline-flex;',
'align-items: center;',
'gap: 6px;',
'transition: background 120ms ease, transform 60ms ease;',
style
].join('');
btn.addEventListener('mouseenter', () => (btn.style.background = 'rgba(230,230,230,1)'));
btn.addEventListener('mouseleave', () => (btn.style.background = 'rgba(240,240,240,0.9)'));
btn.addEventListener('mousedown', () => (btn.style.transform = 'translateY(1px)'));
btn.addEventListener('mouseup', () => (btn.style.transform = 'translateY(0)'));
if (onClick) btn.addEventListener('click', onClick);
return btn;
}
function createIconLabelBtn({ iconSVG, label, onClick, title }) {
const btn = createBtn({ label: '', onClick, title });
const span = document.createElement('span');
span.textContent = label;
btn.insertAdjacentHTML('afterbegin', iconSVG);
btn.appendChild(span);
return btn;
}
function ensureHeader(preEl) {
// If there's a sibling header/toolbar above the <pre>, use it.
// Otherwise, create a minimal toolbar container.
let header = preEl.previousElementSibling;
const looksLikeToolbar =
header &&
(header.tagName === 'DIV' || header.tagName === 'SECTION') &&
header.childElementCount <= 6;
if (!looksLikeToolbar) {
header = document.createElement('div');
header.style.cssText = [
'display:flex;',
'align-items:center;',
'gap:8px;',
'margin: 0 0 6px 0;',
'flex-wrap: wrap;'
].join('');
preEl.parentElement?.insertBefore(header, preEl);
}
return header;
}
// ---------------------------
// Toggle helpers
// ---------------------------
function createToggle(label, target, options = {}) {
const {
showText = 'Show code',
hideText = 'Hide code',
initialHidden = true
} = options;
// Initial state
target.style.display = initialHidden ? 'none' : '';
let hidden = initialHidden;
const btn = createIconLabelBtn({
iconSVG: TOGGLE_SVG,
label: initialHidden ? showText : hideText,
title: 'Toggle code',
onClick: (e) => {
e.stopPropagation();
hidden = !hidden;
target.style.display = hidden ? 'none' : '';
btn.querySelector('span').textContent = hidden ? showText : hideText;
const svg = btn.querySelector('svg');
if (svg) svg.style.transform = hidden ? 'rotate(-90deg)' : 'rotate(0deg)';
}
});
// set initial icon orientation
const svg = btn.querySelector('svg');
if (svg) svg.style.transform = hidden ? 'rotate(-90deg)' : 'rotate(0deg)';
return btn;
}
// ---------------------------
// Processing functions
// ---------------------------
function processAssistantPre(pre) {
if (!pre || pre.dataset.__collapsed) return;
const code = pre.querySelector('code');
if (!code) return;
pre.dataset.__collapsed = '1';
const header = ensureHeader(pre);
// Toggle 'initialHidden' to false if you want code blocks to be expanded by default
// Make sure to also toggle 'initialHidden' in the processUserPre function below
const toggleBtn = createToggle('Show code', code, { showText: 'Show code', hideText: 'Hide code', initialHidden: true });
header.appendChild(toggleBtn);
}
function processUserPre(pre) {
if (!pre || pre.dataset.__collapsed) return;
const code = pre.querySelector('code');
if (!code) return;
pre.dataset.__collapsed = '1';
pre.style.position = pre.style.position || 'relative';
// Controls container
const ctr = document.createElement('div');
ctr.style.cssText = [
'position:absolute;',
'top:6px;',
'right:8px;',
'display:flex;',
'gap:6px;',
'z-index: 2;'
].join('');
pre.appendChild(ctr);
// Copy
const copyBtn = createIconLabelBtn({
iconSVG: COPY_SVG,
label: 'Copy',
title: 'Copy code',
onClick: (e) => {
e.stopPropagation();
const text = code.innerText || code.textContent || '';
navigator.clipboard.writeText(text).then(() => {
const lbl = copyBtn.querySelector('span');
const old = lbl.textContent;
lbl.textContent = 'Copied';
setTimeout(() => (lbl.textContent = old), 900);
});
}
});
// Toggle 'initialHidden' to false if you want code blocks to be expanded by default
const toggleBtn = createToggle('Show', code, { showText: 'Show', hideText: 'Hide', initialHidden: true });
ctr.append(copyBtn, toggleBtn);
}
// ---------------------------
// Scanner
// ---------------------------
function scan() {
// Find all <pre> blocks (assistant & user) and classify by bubble role
const pres = document.querySelectorAll('pre');
pres.forEach((pre) => {
if (pre.dataset.__collapsed) return;
const bubble = pre.closest('[data-message-author-role]');
const role = bubble?.getAttribute('data-message-author-role');
if (role === 'assistant') {
processAssistantPre(pre);
} else if (role === 'user') {
processUserPre(pre);
} else {
// Unknown: treat like assistant (safe default)
processAssistantPre(pre);
}
});
}
// Initial scan
const start = () => {
try {
scan();
} catch (e) {
// no-op
}
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start, { once: true });
} else {
start();
}
// Observe DOM changes to catch new messages/edits/streamed content
const observer = new MutationObserver(debounce(() => scan(), 200));
observer.observe(document.documentElement, { childList: true, subtree: true });
// Re-run on resize (some layouts lazily attach code blocks)
window.addEventListener('resize', debounce(scan, 300));
})();