// ==UserScript==
// @name ChatGPT 바로 삭제
// @namespace https://chatgpt.com/
// @version 1.0.0
// @description ChatGPT 대화목록에 마우스를 올리면 휴지통 아이콘이 나타나고, 클릭 시 확인창 없이 대화를 즉시 삭제합니다.
// @match https://chatgpt.com/*
// @match https://chat.openai.com/*
// @grant none
// @license CC-BY-NC-SA-4.0
// @author guvno
// @source https://greasyfork.org/en/scripts/533597-chatgpt-quick-delete-no-popup
// ==/UserScript==
(() => {
const DEBUG = false; // true로 설정 시 console.log 출력
const waitFor = (pred, ms = 8000, 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 }));
async function deleteConversation(li) {
const dots = li.querySelector(
'button[data-testid$="-options"], button[aria-label="대화 옵션 열기"]'
);
if (DEBUG) console.log('[QD] options button:', dots);
if (!dots) return;
['pointerdown', 'pointerup', 'click'].forEach(t => fire(dots, t));
await waitFor(() => document.querySelector('[role="menu"]'), 8000);
const menuItems = [...document.querySelectorAll('[role="menuitem"], button')];
if (DEBUG) console.log('[QD] all menu items:', menuItems.map(el => el.textContent.trim()));
const del = menuItems.find(el => /삭제/.test(el.textContent));
if (DEBUG) console.log('[QD] matched delete item:', del);
if (!del) return;
['pointerdown', 'pointerup', 'click'].forEach(t => fire(del, t));
const confirm = await waitFor(() => {
return (
document.querySelector('button[data-testid="delete-conversation-confirm-button"]') ||
[...document.querySelectorAll('button')].find(el =>
el.textContent.trim() === '삭제' || el.textContent.includes('삭제')
)
);
}, 8000);
if (DEBUG) console.log('[QD] confirm button:', confirm);
if (!confirm) return;
['pointerdown', 'pointerup', 'click'].forEach(t => fire(confirm, t));
li.style.transition = 'opacity .25s';
li.style.opacity = 0;
setTimeout(() => li.style.display = 'none', 280);
}
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);
}
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);
})();