// ==UserScript==
// @name ChatGPT Quick‑Delete (No Popup)
// @namespace https://chatgpt.com/
// @version 1.2
// @description Hover to reveal a grey trash‑can badge; click it to auto‑delete the conversation instantly (no confirmation popup).
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @grant none
// @author Blackupfreddy
// @license CC-BY-NC-SA-4.0
// ==/UserScript==
(() => {
/* ══════════ helpers ══════════ */
const waitFor = (pred, ms = 4000, step = 70) =>
new Promise(res => {
const end = Date.now() + ms;
(function loop() {
const el = pred();
if (el) return res(el);
if (Date.now() > end) return res(null);
setTimeout(loop, step);
})();
});
const fire = (el, type) =>
el.dispatchEvent(new MouseEvent(type, { bubbles: true, composed: true }));
/* ══════════ delete flow ══════════ */
async function deleteConversation(li) {
const link = li.querySelector('a[data-history-item-link]');
const startPath = location.pathname;
const targetPath = link && link.getAttribute('href');
const stay = targetPath && startPath !== targetPath;
const dots = li.querySelector('button[data-testid$="-options"]');
if (!dots) return;
['pointerdown','pointerup','click'].forEach(t => fire(dots, t));
const del = await waitFor(() =>
[...document.querySelectorAll('[role="menuitem"], button')]
.find(el => /^delete$/i.test(el.textContent.trim()) && !el.closest('.quick‑delete')));
if (!del) return;
['pointerdown','pointerup','click'].forEach(t => fire(del, t));
// **No confirmation step**—deletion is automatic
const confirm = await waitFor(() =>
document.querySelector('button[data-testid="delete-conversation-confirm-button"], .btn-danger'));
if (!confirm) return;
['pointerdown','pointerup','click'].forEach(t => fire(confirm, t));
if (stay) setTimeout(() => history.replaceState(null,'',startPath), 80);
li.style.transition = 'opacity .25s';
li.style.opacity = '0';
setTimeout(() => (li.style.display = 'none'), 280);
}
/* ══════════ icon injection ══════════ */
const ICON = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
viewBox="0 0 24 24" fill="none" stroke="currentColor"
stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6
m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line></svg>`;
function decorate(li) {
if (li.querySelector('.quick‑delete')) return;
const grp = li.querySelector('.group');
const link = grp?.querySelector('a[data-history-item-link]');
if (!grp || !link) return;
grp.style.position = 'relative';
if (!link.dataset.origPad) {
link.dataset.origPad = getComputedStyle(link).paddingLeft || '0px';
}
const icon = Object.assign(document.createElement('span'), {
className : 'quick‑delete',
innerHTML : ICON
});
const bg1 = 'var(--sidebar-surface-secondary, #4b5563)';
const bg2 = 'var(--sidebar-surface-tertiary , #6b7280)';
Object.assign(icon.style, {
position : 'absolute',
left : '4px',
top : '50%',
transform : 'translateY(-50%)',
cursor : 'pointer',
pointerEvents : 'auto',
zIndex : 5,
padding : '2px',
borderRadius : '4px',
background : `linear-gradient(135deg, ${bg1}, ${bg2})`,
color : 'var(--token-text-primary)',
opacity : 0,
transition : 'opacity 100ms'
});
grp.addEventListener('mouseenter', () => {
icon.style.opacity = '.85';
link.style.transition = 'padding-left 100ms';
link.style.paddingLeft = '28px';
});
grp.addEventListener('mouseleave', () => {
icon.style.opacity = '0';
link.style.paddingLeft = link.dataset.origPad;
});
icon.addEventListener('click', e => {
e.stopPropagation();
e.preventDefault();
deleteConversation(li);
});
grp.prepend(icon);
}
/* ══════════ observer ══════════ */
const itemSelector = 'li[data-testid^="history-item-"]';
function handleMutation(records) {
for (const rec of records) {
rec.addedNodes.forEach(node => {
if (node.nodeType === 1 && node.matches(itemSelector)) decorate(node);
else if (node.nodeType === 1) node.querySelectorAll?.(itemSelector).forEach(decorate);
});
}
}
function decorateInBatches(nodes) {
const batch = nodes.splice(0, 50);
batch.forEach(decorate);
if (nodes.length) requestIdleCallback(() => decorateInBatches(nodes));
}
function init() {
const history = document.getElementById('history');
if (!history) return;
new MutationObserver(handleMutation)
.observe(history, { childList: true, subtree: true });
const startNodes = [...history.querySelectorAll(itemSelector)];
if (startNodes.length) requestIdleCallback(() => decorateInBatches(startNodes));
}
const ready = setInterval(() => {
if (document.getElementById('history')) {
clearInterval(ready);
init();
}
}, 150);
})();