ChatGPT Quick‑Delete (No Popup)

Hover to reveal a grey trash‑can badge; click it to auto‑delete the conversation instantly (no confirmation popup).

// ==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);
})();