LibreChat Shortcuts + Token Counter

Press Alt+S to click toggle-left-nav button on localhost:3080

// ==UserScript==
// @name         LibreChat Shortcuts + Token Counter
// @namespace    http://tampermonkey.net/
// @version      2.8.1
// @description  Press Alt+S to click toggle-left-nav button on localhost:3080
// @author       bwhurd
// @match        http://localhost:3080/*
// @grant        none
// @run-at       document-end
// ==/UserScript==

// === Shortcut Keybindings ===
// Alt+S → Toggle sidebar (clicks #toggle-left-nav)
// Alt+N → New chat (clicks button[aria-label="New chat"])
// Alt+T → Scroll to top of message container
// Alt+Z → Scroll to bottom of message container
// Alt+W → Focus Chat Input
// Alt+C → Click Copy on lowest message
// Alt+X → Select and copy, cycles visible messages
// Alt+A → Scroll up one message (.message-render)
// Alt+F → Scroll down one message (.message-render)
// Just start typing to go to input chatbox
// Paste input when not in chat box

// Alt+R → refresh cost for conversation
// Alt+U → update the token cost per million
// Alt+e   →   toggle collapse expand chat
// alt+d   →   Open the preset menu to see the "defaults"
// alt+# 1-9 to activate presets

(function () {
    'use strict';

    // === Inject custom CSS to override hidden footer button color ===
    const style = document.createElement('style');
    style.textContent = `
        .relative.hidden.items-center.justify-center {
            display:none;
        }
    `;
    document.head.appendChild(style);

    // Shared scroll state object
    const ScrollState = {
        scrollContainer: null,
        isAnimating: false,
        finalScrollPosition: 0,
        userInterrupted: false,
    };

    function resetScrollState() {
        if (ScrollState.isAnimating) {
            ScrollState.isAnimating = false;
            ScrollState.userInterrupted = true;
        }
        ScrollState.scrollContainer = getScrollableContainer();
        if (ScrollState.scrollContainer) {
            ScrollState.finalScrollPosition = ScrollState.scrollContainer.scrollTop;
        }
    }

    function getScrollableContainer() {
        const firstMessage = document.querySelector('.message-render');
        if (!firstMessage) return null;

        let container = firstMessage.parentElement;
        while (container && container !== document.body) {
            const style = getComputedStyle(container);
            if (
                container.scrollHeight > container.clientHeight &&
                style.overflowY !== 'visible' &&
                style.overflowY !== 'hidden'
            ) {
                return container;
            }
            container = container.parentElement;
        }

        return document.scrollingElement || document.documentElement;
    }

    function checkGSAP() {
        if (
            typeof window.gsap !== "undefined" &&
            typeof window.ScrollToPlugin !== "undefined" &&
            typeof window.Observer !== "undefined" &&
            typeof window.Flip !== "undefined"
        ) {
            gsap.registerPlugin(ScrollToPlugin, Observer, Flip);
            console.log("✅ GSAP and plugins registered");
            initShortcuts();
        } else {
            console.warn("⏳ GSAP not ready. Retrying...");
            setTimeout(checkGSAP, 100);
        }
    }

    function loadGSAPLibraries() {
        const libs = [
            'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/gsap.min.js',
            'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/ScrollToPlugin.min.js',
            'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/Observer.min.js',
            'https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.7/Flip.min.js',
        ];

        libs.forEach(src => {
            const script = document.createElement('script');
            script.src = src;
            script.async = false;
            document.head.appendChild(script);
        });

        checkGSAP();
    }

    function scrollToTop() {
        const container = getScrollableContainer();
        if (!container) return;
        gsap.to(container, {
            duration: .6,
            scrollTo: { y: 0 },
            ease: "power4.out"
        });
    }

    function scrollToBottom() {
        const container = getScrollableContainer();
        if (!container) return;
        gsap.to(container, {
            duration: .6,
            scrollTo: { y: "max" },
            ease: "power4.out"
        });
    }

    function scrollUpOneMessage() {
        const container = getScrollableContainer();
        if (!container) return;

        const messages = [...document.querySelectorAll('.message-render')];
        const currentScrollTop = container.scrollTop;

        let target = null;
        for (let i = messages.length - 1; i >= 0; i--) {
            if (messages[i].offsetTop < currentScrollTop - 25) {
                target = messages[i];
                break;
            }
        }

        gsap.to(container, {
            duration: 0.6,
            scrollTo: { y: target?.offsetTop || 0 },
            ease: "power4.out"
        });
    }

    function scrollDownOneMessage() {
        const container = getScrollableContainer();
        if (!container) return;

        const messages = [...document.querySelectorAll('.message-render')];
        const currentScrollTop = container.scrollTop;

        let target = null;
        for (let i = 0; i < messages.length; i++) {
            if (messages[i].offsetTop > currentScrollTop + 25) {
                target = messages[i];
                break;
            }
        }

        gsap.to(container, {
            duration: 0.6,
            scrollTo: { y: target?.offsetTop || container.scrollHeight },
            ease: "power4.out"
        });
    }

    function initShortcuts() {
        document.addEventListener('keydown', function (e) {
            if (!e.altKey || e.repeat) return;

            const key = e.key.toLowerCase();

            const keysToBlock = ['s', 'n', 't', 'z', 'a', 'f'];
            if (keysToBlock.includes(key)) {
                e.preventDefault();
                e.stopPropagation();

                switch (key) {
                    case 's': toggleSidebar(); break;
                    case 'n': openNewChat(); break;
                    case 't': scrollToTop(); break;
                    case 'z': scrollToBottom(); break;
                    case 'a': scrollUpOneMessage(); break;
                    case 'f': scrollDownOneMessage(); break;
                }
            }
        });

        console.log("✅ LibreChat shortcuts active");
    }

    function toggleSidebar() {
        const toggleButton = document.getElementById('toggle-left-nav');
        if (toggleButton) {
            toggleButton.click();
            console.log('🧭 Sidebar toggled');
        }
    }

    function openNewChat() {
        const newChatButton = document.querySelector('button[aria-label="New chat"]');
        if (newChatButton) {
            newChatButton.click();
            console.log('🆕 New chat opened');
        }
    }

    // Start loading GSAP plugins and wait for them
    loadGSAPLibraries();
})();




(function() {
    document.addEventListener('keydown', function(e) {
        if (e.altKey && e.key === 'w') {
            e.preventDefault();
            const chatInput = document.querySelector('#prompt-textarea');
            if (chatInput) {
                chatInput.focus();
            }
        }
    });
})();

(function() {
    function removeMarkdown(text) {
        return text
        // Remove bold/italics
            .replace(/(\*\*|__)(.*?)\1/g, "$2")
            .replace(/(\*|_)(.*?)\1/g, "$2")
        // Remove leading '#' from headers
            .replace(/^#{1,6}\s+(.*)/gm, "$1")
        // Preserve indentation for unordered list items
            .replace(/^(\s*)[\*\-\+]\s+(.*)/gm, "$1- $2")
        // Preserve indentation for ordered list items
            .replace(/^(\s*)(\d+)\.\s+(.*)/gm, "$1$2. $3")
        // Remove triple+ line breaks
            .replace(/\n{3,}/g, "\n\n")
            .trim();
    }

    document.addEventListener('keydown', function(e) {
        if (e.altKey && e.key === 'c') {
            e.preventDefault();
            const allButtons = Array.from(document.querySelectorAll('button'));
            const visibleButtons = allButtons.filter(button =>
                                                     button.innerHTML.includes('M7 5a3 3 0 0 1 3-3h9a3')
                                                    ).filter(button => {
                const rect = button.getBoundingClientRect();
                return (
                    rect.top >= 0 &&
                    rect.left >= 0 &&
                    rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                    rect.right <= (window.innerWidth || document.documentElement.clientWidth)
                );
            });

            if (visibleButtons.length > 0) {
                visibleButtons[visibleButtons.length - 1].click();

                setTimeout(() => {
                    if (!navigator.clipboard) return;

                    navigator.clipboard.readText()
                        .then(textContent => navigator.clipboard.writeText(removeMarkdown(textContent)))
                        .then(() => console.log("Markdown removed and copied."))
                        .catch(() => {});
                }, 500);
            }
        }
    });
})();

(function() {
    // Initialize single global store for last selection
    window.selectAllLowestResponseState = window.selectAllLowestResponseState || {
        lastSelectedIndex: -1
    };

    document.addEventListener('keydown', function(e) {
        if (e.altKey && e.key === 'x') {
            e.preventDefault();
            // Delay execution to ensure DOM is fully loaded
            setTimeout(() => {
                try {
                    const onlySelectAssistant = window.onlySelectAssistantCheckbox || false;
                    const onlySelectUser = window.onlySelectUserCheckbox || false;
                    const disableCopyAfterSelect = window.disableCopyAfterSelectCheckbox || false;

                    const allConversationTurns = (() => {
                        try {
                            return Array.from(document.querySelectorAll('.user-turn, .agent-turn')) || [];
                        } catch {
                            return [];
                        }
                    })();

                    const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
                    const viewportWidth = window.innerWidth || document.documentElement.clientWidth;

                    const composerRect = (() => {
                        try {
                            const composerBackground = document.getElementById('composer-background');
                            return composerBackground ? composerBackground.getBoundingClientRect() : null;
                        } catch {
                            return null;
                        }
                    })();

                    const visibleTurns = allConversationTurns.filter(el => {
                        const rect = el.getBoundingClientRect();
                        const horizontallyInView = rect.left < viewportWidth && rect.right > 0;
                        const verticallyInView = rect.top < viewportHeight && rect.bottom > 0;
                        if (!horizontallyInView || !verticallyInView) return false;

                        if (composerRect) {
                            if (rect.top >= composerRect.top) {
                                return false;
                            }
                        }

                        return true;
                    });

                    const filteredVisibleTurns = (() => {
                        if (onlySelectAssistant) {
                            return visibleTurns.filter(el =>
                                                       el.querySelector('[data-message-author-role="assistant"]')
                                                      );
                        }
                        if (onlySelectUser) {
                            return visibleTurns.filter(el =>
                                                       el.querySelector('[data-message-author-role="user"]')
                                                      );
                        }
                        return visibleTurns;
                    })();

                    if (filteredVisibleTurns.length === 0) return;

                    filteredVisibleTurns.sort((a, b) => {
                        const ra = a.getBoundingClientRect();
                        const rb = b.getBoundingClientRect();
                        return rb.top - ra.top;
                    });

                    const { lastSelectedIndex } = window.selectAllLowestResponseState;
                    const nextIndex = (lastSelectedIndex + 1) % filteredVisibleTurns.length;
                    const selectedTurn = filteredVisibleTurns[nextIndex];
                    if (!selectedTurn) return;

                    selectAndCopyMessage(selectedTurn);
                    window.selectAllLowestResponseState.lastSelectedIndex = nextIndex;

                    function selectAndCopyMessage(turnElement) {
                        try {
                            const userContainer = turnElement.querySelector('[data-message-author-role="user"]');
                            const isUser = !!userContainer;

                            if (isUser) {
                                if (onlySelectAssistant) return;
                                const userTextElement = userContainer.querySelector('.whitespace-pre-wrap');
                                if (!userTextElement) return;
                                doSelectAndCopy(userTextElement);
                            } else {
                                if (onlySelectUser) return;
                                const assistantContainer = turnElement.querySelector('[data-message-author-role="assistant"]');
                                let textElement = null;
                                if (assistantContainer) {
                                    textElement = assistantContainer.querySelector('.prose') || assistantContainer;
                                } else {
                                    textElement = turnElement.querySelector('.prose') || turnElement;
                                }
                                if (!textElement) return;
                                doSelectAndCopy(textElement);
                            }
                        } catch {
                            // Fail silently
                        }
                    }

                    function doSelectAndCopy(el) {
                        try {
                            const selection = window.getSelection();
                            if (!selection) return;
                            selection.removeAllRanges();

                            const range = document.createRange();
                            range.selectNodeContents(el);
                            selection.addRange(range);

                            if (!disableCopyAfterSelect) {
                                document.execCommand('copy');
                            }
                        } catch {
                            // Fail silently
                        }
                    }

                } catch {
                    // Fail silently
                }
            }, 50);
        }
    });
})();
// Existing script functionalities...
(function() {
    const controlsNavId = 'controls-nav';
    const chatInputId = 'prompt-textarea';

    // Function to handle focusing and manually pasting into the chat input
    function handlePaste(e) {
        const chatInput = document.getElementById(chatInputId);
        if (!chatInput) return;

        // Focus the input if it is not already focused
        if (document.activeElement !== chatInput) {
            chatInput.focus();
        }

        // Use a small delay to ensure focus happens before insertion
        setTimeout(() => {
            // Prevent default paste action to manually handle paste
            e.preventDefault();

            // Obtain the pasted text
            const pastedData = (e.clipboardData || window.clipboardData).getData('text') || '';

            const cursorPosition = chatInput.selectionStart;
            const textBefore = chatInput.value.substring(0, cursorPosition);
            const textAfter = chatInput.value.substring(cursorPosition);

            // Set the new value with pasted data
            chatInput.value = textBefore + pastedData + textAfter;

            // Move the cursor to the end of inserted data
            chatInput.selectionStart = chatInput.selectionEnd = cursorPosition + pastedData.length;

            // Trigger an 'input' event to ensure any form listeners react
            const inputEvent = new Event('input', { bubbles: true, cancelable: true });
            chatInput.dispatchEvent(inputEvent);
        }, 0);
    }

    document.addEventListener('paste', function(e) {
        const activeElement = document.activeElement;

        // If currently focused on a textarea/input that is NOT our chat input, do nothing
        if (
            (activeElement.tagName.toLowerCase() === 'textarea' || activeElement.tagName.toLowerCase() === 'input') &&
            activeElement.id !== chatInputId
        ) {
            return;
        }

        // If currently within #controls-nav, do nothing
        if (activeElement.closest(`#${controlsNavId}`)) {
            return;
        }

        // Otherwise, handle the paste event
        handlePaste(e);
    });
})();

(function() {
    const controlsNavId = 'controls-nav';
    const chatInputId = 'prompt-textarea';

    document.addEventListener('keydown', function(e) {
        const activeElement = document.activeElement;

        // If focused on any other textarea/input besides our chat input, do nothing
        if (
            (activeElement.tagName.toLowerCase() === 'textarea' || activeElement.tagName.toLowerCase() === 'input') &&
            activeElement.id !== chatInputId
        ) {
            return;
        }

        // If currently within #controls-nav, do nothing
        if (activeElement.closest(`#${controlsNavId}`)) {
            return;
        }

        // Check if the pressed key is alphanumeric and no modifier keys are pressed
        const isAlphanumeric = e.key.length === 1 && /[a-zA-Z0-9]/.test(e.key);
        const isModifierKeyPressed = e.altKey || e.ctrlKey || e.metaKey; // metaKey for Cmd on Mac

        if (isAlphanumeric && !isModifierKeyPressed) {
            const chatInput = document.getElementById(chatInputId);
            if (!chatInput) return;

            // If we're not already in our chat input, focus it and add the character
            if (activeElement !== chatInput) {
                e.preventDefault();
                chatInput.focus();
                chatInput.value += e.key;
            }
        }
    });
})();






/*=============================================================
=                                                             =
=  Token counter IIFE                                         =
=                                                             =
=============================================================*/
(function(){
    'use strict';

    // ——— Keys & defaults ———
    const COST_IN_KEY          = 'costInput';
    const COST_OUT_KEY         = 'costOutput';
    const CPT_KEY              = 'charsPerToken';
    let costIn       = parseFloat(localStorage.getItem(COST_IN_KEY))  || 2.50;
    let costOut      = parseFloat(localStorage.getItem(COST_OUT_KEY)) || 10.00;
    let charsPerTok  = parseFloat(localStorage.getItem(CPT_KEY))     || 3.8;
    const OVERHEAD   = 3; // tokens per message overhead

    // ——— Estimator ———
    function estTok(text){
        return Math.ceil((text.trim().length||0)/charsPerTok) + OVERHEAD;
    }

    // ——— UI: badge + refresh button ———
    const badge = document.createElement('span');
    badge.id = 'token-count-badge';
    Object.assign(badge.style, {
        fontSize:'8px', padding:'1px 0 0 6px', borderRadius:'8px',
        background:'transparent', color:'#a9a9a9',
        fontFamily:'monospace', userSelect:'none',
        alignSelf:'center', marginTop:'16px',
        display:'inline-flex', alignItems:'center'
    });

    const refreshBtn = document.createElement('button');
    refreshBtn.textContent = '↻';
    refreshBtn.title   = 'Refresh token count';
    Object.assign(refreshBtn.style, {
        marginLeft:'6px', cursor:'pointer', fontSize:'10px',
        border:'none', background:'transparent',
        color:'#a9a9a9', userSelect:'none',
        fontFamily:'monospace', padding:'0'
    });
    refreshBtn.addEventListener('click', ()=>{
        flash(refreshBtn);
        updateCounts();
    });
    badge.appendChild(refreshBtn);

    function flash(el){
        el.style.transition = 'transform 0.15s';
        el.style.transform  = 'scale(1.4)';
        setTimeout(()=> el.style.transform = 'scale(1)', 150);
    }

    // ——— Inject badge in the “flex row” before mic button ———
    function insertBadge(retries=20){
        const rows = [...document.querySelectorAll('div.flex')];
        const flexRow = rows.find(el =>
                                  el.classList.contains('items-between') &&
                                  el.classList.contains('pb-2')
                                 );
        if(!flexRow){
            if(retries>0) setTimeout(()=> insertBadge(retries-1), 500);
            return null;
        }
        if(!flexRow.querySelector('#token-count-badge')){
            const mic = flexRow.querySelector('button[title="Use microphone"]');
            flexRow.insertBefore(badge, mic);
        }
        return flexRow.parentElement;
    }

    // ——— Role inference ———
    function inferRole(msgEl){
        const wrapper = msgEl.closest('.group, .message');
        if(wrapper?.classList.contains('user'))      return 'user';
        if(wrapper?.classList.contains('assistant')) return 'assistant';
        const all = [...document.querySelectorAll('.message-render')];
        return all.indexOf(msgEl)%2===0 ? 'user' : 'assistant';
    }

    // ——— Core update logic ———
    function updateCounts(){
        const msgs = [...document.querySelectorAll('.message-render')];
        if(!msgs.length){
            badge.textContent = '0 | 0 | ∑ 0 | $0.0000';
            badge.appendChild(refreshBtn);
            return;
        }
        const convo = msgs.map(m => ({
            role: inferRole(m),
            t:    estTok(m.innerText||'')
        }));
        let inSum=0, outSum=0;
        for(let i=0;i<convo.length;i++){
            if(convo[i].role==='user'){
                inSum += convo.slice(0,i+1).reduce((a,b)=>a+b.t,0);
                const ai = convo.findIndex((c,j)=>j>i&&c.role==='assistant');
                if(ai>i) outSum += convo[ai].t;
            }
        }
        const total = inSum+outSum;
        const cost  = ((inSum/1e6)*costIn + (outSum/1e6)*costOut).toFixed(4);
        badge.textContent = `${inSum} @ $${costIn}/M | ${outSum} @ $${costOut}/M | ∑ ${total} | $${cost}`;
        badge.appendChild(refreshBtn);
    }

    // ——— Debounce for MutationObserver ———
    let debounceTimer=null;
    function scheduleUpdate(){
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(updateCounts, 200);
    }

    // ——— Hook send actions for immediate update ———
    function attachSendHooks(){
        const ta = document.querySelector('textarea');
        if(ta && !ta.dataset.tcHooked){
            ta.dataset.tcHooked = 'y';
            ta.addEventListener('keydown', e=>{
                if(e.key==='Enter' && !e.shiftKey && !e.altKey && !e.metaKey){
                    scheduleUpdate();
                }
            });
        }
        const send = document.querySelector('button[type="submit"], button[title="Send"]');
        if(send && !send.dataset.tcHooked){
            send.dataset.tcHooked = 'y';
            send.addEventListener('click', ()=> scheduleUpdate());
        }
    }

    // ——— Initialization ———
    function init(){
        const container = insertBadge();
        if(!container) return;
        // observe only the messages container
        const msgRoot = container.querySelector('.message-render')?.parentElement || container;
        new MutationObserver(scheduleUpdate)
            .observe(msgRoot, { childList:true, subtree:true });
        attachSendHooks();
        // reattach hooks if textarea/send are re-rendered
        new MutationObserver(attachSendHooks)
            .observe(document.body, { childList:true, subtree:true });
        updateCounts();
    }

    // ——— Config shortcut (Alt+U) ———
    document.addEventListener('keydown', e=>{
        if(e.altKey && !e.repeat && e.key.toLowerCase()==='u'){
            e.preventDefault();
            const resp = prompt(
                'Set costs and chars/token:\ninput $/M,output $/M,chars/token',
                `${costIn},${costOut},${charsPerTok}`
      );
            if(!resp) return;
            const [ci,co,cpt] = resp.split(',').map(Number);
            if([ci,co,cpt].every(v=>isFinite(v))){
                costIn = ci; costOut = co; charsPerTok = cpt;
                localStorage.setItem(COST_IN_KEY,ci);
                localStorage.setItem(COST_OUT_KEY,co);
                localStorage.setItem(CPT_KEY,cpt);
                updateCounts();
            } else alert('Invalid numbers');
        }
    });

    // delay to let page render
    setTimeout(init, 1000);

})();



// Convert <br> in tables displaying as literal <br> to line breaks
(function () {
    'use strict';

    const BR_ENTITY_REGEX = /&lt;br\s*\/?&gt;/gi;

    function fixBrsInMarkdown() {
        document.querySelectorAll('div.markdown').forEach(container => {
            container.querySelectorAll('td, th, p, li, div').forEach(el => {
                if (el.innerHTML.includes('&lt;br')) {
                    el.innerHTML = el.innerHTML.replace(BR_ENTITY_REGEX, '<br>');
                }
            });
        });
    }

    // Run once in case content is already loaded
    fixBrsInMarkdown();

    // Watch for content changes
    const observer = new MutationObserver(() => fixBrsInMarkdown());

    observer.observe(document.body, {
        childList: true,
        subtree: true,
    });
})();


// Alt+e   →   toggle collapse expand chat
(function() {
    document.addEventListener('keydown', function(e) {
        if (e.altKey && e.key.toLowerCase() === 'e') {
            e.preventDefault();
            const collapseBtn = document.querySelector('button[aria-label="Collapse Chat"]');
            if (collapseBtn) {
                collapseBtn.click();
                return;
            }
            const expandBtn = document.querySelector('button[aria-label="Expand Chat"]');
            if (expandBtn) expandBtn.click();
        }
    });
})();

// alt+1 to alt+9 to select presets
(() => {
  const synthClick = el => {
    if (!el) return;
    el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
    el.focus?.();
  };

  function handleAltDigit(ev) {
    if (!ev.altKey || !/^Digit[1-9]$/.test(ev.code)) return;
    ev.preventDefault();
    ev.stopPropagation();

    const idx = +ev.code.slice(-1) - 1;
    const btn = document.getElementById('presets-button');
    if (!btn) return console.warn('[Preset-helper] #presets-button not found');

    btn.click();
    setTimeout(() => {
      const items = Array.from(
        document.querySelectorAll('div[role="option"][data-testid^="preset-item"]')
      );
      if (items[idx]) synthClick(items[idx]);
    }, 500);
  }

  window.addEventListener('keydown', handleAltDigit, true);
})();


// Label the presets

(() => {
  /* ——— simple style for the tiny label ——— */
  const style = document.createElement('style');
  style.textContent = `
    .alt-hint {
      font-size: 10px;          /* small text */
      opacity: .5;              /* 50 % opacity  */
      margin-left: 4px;         /* a little gap  */
      pointer-events: none;     /* never blocks clicks */
      user-select: none;
    }`;
  document.head.appendChild(style);

  const ITEM_SELECTOR = 'div[role="option"][data-testid^="preset-item"]';
  const MAX_DIGITS = 9; // Alt+1 … Alt+9

  /** add the hint to each item (if not already present) */
  const addHints = () => {
    [...document.querySelectorAll(ITEM_SELECTOR)]
      .slice(0, MAX_DIGITS)
      .forEach((el, i) => {
        if (el.querySelector('.alt-hint')) return;      // only once
        const span = document.createElement('span');
        span.className = 'alt-hint';
        span.textContent = `Alt+${i + 1}`;
        el.appendChild(span);
      });
  };

  /* run once right now (in case the menu is already open) */
  addHints();

  /* keep watching for future openings of the menu */
  const mo = new MutationObserver(addHints);
  mo.observe(document.body, { childList: true, subtree: true });
})();



// alt+d   →   Open the preset menu

(() => {
  'use strict';

  window.addEventListener('keydown', handleAltP, true);

  const openPresetMenu = () => {
    const btn = document.getElementById('presets-button');
    if (!btn) {
      console.log('[Preset-helper] couldn’t find #presets-button');
      return false;
    }
    btn.click();
    return true;
  };

  function handleAltP(e) {
    if (e.altKey && e.code === 'KeyD') {
      e.preventDefault();
      e.stopPropagation();
      openPresetMenu();
    }
  }
})();