Claude Style Selector v3.1.0

← Back to User Scripts

Description for Style Selector v3.1.0.

Script Content

// ==UserScript==
// @name         Claude Style Selector
// @namespace    https://github.com/tjhleeds/user-scripts/
// @version      3.1.0
// @description  Display and select Claude conversation styles from a sidebar with keyboard shortcuts
// @author       tjhleeds using Claude, Jules, and GitHub Copilot
// @match        https://claude.ai/
// @match        https://claude.ai/new
// @match        https://claude.ai/chat/*
// @match        https://claude.ai/project/*
// @grant        unsafeWindow
// ==/UserScript==

(function() {
    'use strict';

    const OBSERVER_TIMEOUT_MS = 10000;
    const ELEMENT_READY_DELAY_MS = 50;
    let styles = [];
    let defaultStyles = [];
    let customStyles = [];
    let sidebarElement = null;
    let showButtonElement = null;
    let stylesDiscovered = false;

    function hideSidebar() {
        if (sidebarElement) {
            sidebarElement.style.display = 'none';
        }
        if (showButtonElement) {
            showButtonElement.style.display = 'block';
        }
        document.documentElement.style.marginRight = '0';
        localStorage.setItem('claude-style-selector-sidebar-hidden', 'true');
    }

    function showSidebar() {
        if (sidebarElement) {
            sidebarElement.style.display = 'block';
        }
        if (showButtonElement) {
            showButtonElement.style.display = 'none';
        }
        document.documentElement.style.marginRight = '280px';
        localStorage.setItem('claude-style-selector-sidebar-hidden', 'false');
    }

    /**
     * Wait for an element to appear in the DOM using MutationObserver
     * @param {Function} checkFn - Function that returns the element when found, or null/undefined
     * @param {number} timeoutMs - Timeout in milliseconds
     * @returns {Promise} Resolves with the element, rejects on timeout
     */
    function waitForElement(checkFn, timeoutMs = OBSERVER_TIMEOUT_MS) {
        return new Promise((resolve, reject) => {
            // Check if element already exists
            const element = checkFn();
            if (element) {
                // Add delay before resolving to ensure element is fully rendered
                setTimeout(() => resolve(element), ELEMENT_READY_DELAY_MS);
                return;
            }

            let timeoutId;
            const observer = new MutationObserver(() => {
                const element = checkFn();
                if (element) {
                    clearTimeout(timeoutId);
                    observer.disconnect();
                    // Add delay before resolving to ensure element is fully rendered
                    setTimeout(() => resolve(element), ELEMENT_READY_DELAY_MS);
                }
            });

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

            timeoutId = setTimeout(() => {
                observer.disconnect();
                reject('Timeout waiting for element');
            }, timeoutMs);
        });
    }

    function getCurrentlySelectedStyle() {
        // Check which style button is currently pressed
        const buttons = document.querySelectorAll('button[aria-pressed="true"]');
        for (const button of buttons) {
            const svgPath = button.querySelector('svg path[d*="M15.5117"]');
            if (svgPath) {
                // This is a pressed style button - find the style name
                const p = button.querySelector('p');
                return p ? p.textContent.trim() : null;
            }
        }
        return null;
    }

    async function selectStyle(styleName) {
        console.log(`[StyleSelector] Attempting to select style: "${styleName}"`);
        try {
            // Open the tools menu
            console.log("[StyleSelector] Looking for 'Open tools menu' button...");
            const openToolsMenuButton = document.querySelector('[aria-label="Open tools menu"]');
            if (!openToolsMenuButton) {
                console.error("[StyleSelector] 'Open tools menu' button not found.");
                throw new Error('Tools menu button not found');
            }
            console.log("[StyleSelector] Found 'Open tools menu' button. Clicking it.");
            openToolsMenuButton.click();

            // Wait for style button to appear AND be visible
            console.log("[StyleSelector] Waiting for style button to appear and be visible...");
            const styleButton = await waitForElement(() => {
                const buttons = document.querySelectorAll('button');
                for (const button of buttons) {
                    const svgPath = button.querySelector('svg path[d*="M15.5117"]');
                    if (svgPath && button.offsetParent !== null) {
                        return button;
                    }
                }
                return null;
            });
            console.log("[StyleSelector] Found style button. Clicking it.");
            styleButton.click();

            // NEW: Handle intermediate menu when a style is already selected
            try {
                const useStyleButton = await waitForElement(() => {
                    const allButtons = document.querySelectorAll('button');
                    for (const btn of allButtons) {
                        const p = btn.querySelector('p');
                        if (p && p.textContent.trim() === 'Use style' && btn.offsetParent !== null) {
                            return btn;
                        }
                    }
                    return null;
                }, 1000); // Short timeout because this menu should appear instantly

                if (useStyleButton) {
                    console.log("[StyleSelector] Intermediate menu detected. Clicking 'Use style'.");
                    useStyleButton.click();
                }
            } catch (error) {
                // This is expected if no style is currently selected.
                console.log("[StyleSelector] 'Use style' button not found, assuming style list is already visible.");
            }

            // Wait for the specific style to appear in the submenu and be visible
            console.log(`[StyleSelector] Waiting for style "${styleName}" to appear in the submenu...`);
            const foundButton = await waitForElement(() => {
                const paragraphs = document.querySelectorAll('p');
                console.log(`Paragraphs found: `, paragraphs);
                for (const p of paragraphs) {
                    console.log(`Paragraph textContent: ${p.textContent}`)
                    if (p.textContent.trim() === styleName) {
                        console.log(`Paragraph found with style ${styleName}`);
                        const button = p.closest('button');
                        if (button && button.offsetParent !== null) return button;
                    }
                }
                return null;
            });
            console.log(`[StyleSelector] Found style button for "${styleName}". Clicking it.`);
            foundButton.click();
            console.log(`[StyleSelector] Successfully selected style: "${styleName}"`);

            // Wait for prompt textarea to be available and click it
            console.log("[StyleSelector] Waiting for prompt textarea to be available...");
            await waitForElement(() => document.querySelector('[aria-label="Write your prompt to Claude"]'), 2000);
            const promptTextarea = document.querySelector('[aria-label="Write your prompt to Claude"]');
            if (promptTextarea) {
                console.log("[StyleSelector] Found prompt textarea. Clicking it to focus.");
                promptTextarea.click();
            } else {
                console.warn("[StyleSelector] Prompt textarea not found after waiting.");
            }
        } catch (error) {
            console.error(`[StyleSelector] An error occurred while selecting style "${styleName}":`, error);
            const openToolsMenuButton = document.querySelector('[aria-label="Open tools menu"]');
            if (openToolsMenuButton) {
                console.log("[StyleSelector] Attempting to close the menu on error.");
                openToolsMenuButton.click(); // Try to close menu on error
            }
            throw error;
        }
    }

    function updateSidebarSelection() {
        if (!sidebarElement) return;

        const currentStyle = getCurrentlySelectedStyle();
        const listItems = sidebarElement.querySelectorAll('li');

        for (const item of listItems) {
            const styleName = item.getAttribute('data-style-name');
            const nameSpan = item.querySelector('span:first-child');
            if (nameSpan) {
                if (styleName === currentStyle) {
                    item.style.color = '#fff';
                    nameSpan.style.color = '#fff';
                    nameSpan.style.fontWeight = '600';
                } else {
                    item.style.color = '#ccc';
                    nameSpan.style.color = '#ccc';
                    nameSpan.style.fontWeight = '400';
                }
            }
        }
    }

    function setupKeyboardShortcuts() {
        // Safe letters for default styles (avoiding E, U, I, O due to system conflicts)
        const defaultStyleKeys = ['q', 'w', 'r', 't', 'y', 'p'];

        const handleKeyEvent = async (event) => {
            // Check for Ctrl+Alt+Q,W,R,T,Y,P (default styles, avoiding problematic letters)
            if (event.ctrlKey && event.altKey && !event.shiftKey && defaultStyleKeys.includes(event.key.toLowerCase())) {
                event.preventDefault();
                event.stopPropagation();
                event.stopImmediatePropagation();
                const styleIndex = defaultStyleKeys.indexOf(event.key.toLowerCase());
                if (styleIndex < defaultStyles.length) {
                    const styleName = defaultStyles[styleIndex];
                    console.log(`[StyleSelector] Keyboard shortcut triggered for default style: "${styleName}" (Ctrl+Alt+${event.key.toUpperCase()})`);
                    try {
                        await selectStyle(styleName);
                        updateSidebarSelection();
                    } catch (error) {
                        console.error('Claude Style Selector: Failed to select style via keyboard shortcut:', error);
                    }
                }
                return false;
            }

            // Check for Ctrl+Alt+1,2,3,5,6,7,8,9 (custom styles, skipping 4)
            const allowedNumbers = ['1', '2', '3', '5', '6', '7', '8', '9'];
            if (event.ctrlKey && event.altKey && !event.shiftKey && allowedNumbers.includes(event.key)) {
                event.preventDefault();
                event.stopPropagation();
                event.stopImmediatePropagation();
                // Map key to style index (1->0, 2->1, 3->2, 5->3, 6->4, etc.)
                const keyToIndex = {'1': 0, '2': 1, '3': 2, '5': 3, '6': 4, '7': 5, '8': 6, '9': 7};
                const styleIndex = keyToIndex[event.key];
                if (styleIndex < customStyles.length) {
                    const styleName = customStyles[styleIndex];
                    console.log(`[StyleSelector] Keyboard shortcut triggered for custom style: "${styleName}" (Ctrl+Alt+${event.key})`);
                    try {
                        await selectStyle(styleName);
                        updateSidebarSelection();
                    } catch (error) {
                        console.error('Claude Style Selector: Failed to select style via keyboard shortcut:', error);
                    }
                }
                return false;
            }
        };

        // Add event listeners with capture flag to intercept events early
        document.addEventListener('keydown', handleKeyEvent, true);
        window.addEventListener('keydown', handleKeyEvent, true);

        console.log('Claude Style Selector: Keyboard shortcuts registered.');
    }

    function onStylesDiscovered(discoveredDefaultStyles, discoveredCustomStyles) {
        if (stylesDiscovered || (discoveredDefaultStyles.length === 0 && discoveredCustomStyles.length === 0)) return;
        stylesDiscovered = true;

        console.log('Claude Style Selector: Discovered default styles:', discoveredDefaultStyles);
        console.log('Claude Style Selector: Discovered custom styles:', discoveredCustomStyles);
        defaultStyles = discoveredDefaultStyles;
        customStyles = discoveredCustomStyles;
        styles = [...discoveredDefaultStyles, ...discoveredCustomStyles];

        // Set up keyboard shortcuts now that we know the styles
        setupKeyboardShortcuts();

        // Wait for chat interface to be ready before creating sidebar
        waitForElement(() => document.querySelector('[aria-label="Send message"]'))
            .then(() => {
                console.log('Claude Style Selector: Chat interface loaded, creating sidebar.');
                if (sidebarElement) {
                    sidebarElement.remove();
                }
                createSidebar();
                if (!showButtonElement) {
                    createShowButton();
                }

                // Check if sidebar was previously hidden
                if (localStorage.getItem('claude-style-selector-sidebar-hidden') === 'true') {
                    hideSidebar();
                } else {
                    showSidebar();
                }
            })
            .catch(error => {
                 console.error('Claude Style Selector: Chat interface not found, cannot create sidebar.', error);
            });
    }

    function setupFetchInterceptor() {
        const originalFetch = unsafeWindow.fetch;
        const styleListUrlRegex = /https:\/\/claude\.ai\/api\/organizations\/[a-f0-9-]+\/list_styles/;

        unsafeWindow.fetch = async (...args) => {
            const [url] = args;
            const response = await originalFetch(...args);

            if (typeof url === 'string' && styleListUrlRegex.test(url)) {
                try {
                    const clonedResponse = response.clone();
                    const data = await clonedResponse.json();

                    const discoveredDefaultStyles = data.defaultStyles.map(s => s.name);
                    const discoveredCustomStyles = data.customStyles.map(s => s.name);

                    onStylesDiscovered(discoveredDefaultStyles, discoveredCustomStyles);
                } catch (error) {
                    console.error('Claude Style Selector: Error parsing styles API response:', error);
                }
            }

            return response;
        };
        console.log('Claude Style Selector: Fetch interceptor set up.');
    }

    function createStyleListItem(styleName, shortcut = null) {
        const listItem = document.createElement('li');
        listItem.setAttribute('data-style-name', styleName);
        listItem.style.cssText = `
            color: #ccc;
            padding: 8px 0;
            font-size: 14px;
            cursor: pointer;
            transition: color 0.2s;
            display: flex;
            justify-content: space-between;
            align-items: center;
        `;

        const nameSpan = document.createElement('span');
        nameSpan.textContent = styleName;
        nameSpan.style.cssText = `
            flex: 1;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        `;
        listItem.appendChild(nameSpan);

        if (shortcut) {
            const shortcutSpan = document.createElement('span');
            shortcutSpan.textContent = shortcut;
            shortcutSpan.style.cssText = `
                font-size: 11px;
                color: #666;
                margin-left: 8px;
                font-family: monospace;
            `;
            listItem.appendChild(shortcutSpan);
        }

        // Add click handler
        listItem.addEventListener('click', async () => {
            try {
                await selectStyle(styleName);
                updateSidebarSelection();
            } catch (error) {
                console.error('Claude Style Selector: Failed to select style:', error);
            }
        });

        // Add hover effect
        listItem.addEventListener('mouseenter', () => {
            const nameSpan = listItem.querySelector('span:first-child');
            if (nameSpan && nameSpan.style.color !== 'rgb(255, 255, 255)') {
                listItem.style.color = '#fff';
                nameSpan.style.color = '#fff';
            }
        });
        listItem.addEventListener('mouseleave', () => {
            const nameSpan = listItem.querySelector('span:first-child');
            if (nameSpan && nameSpan.style.fontWeight !== '600') {
                listItem.style.color = '#ccc';
                nameSpan.style.color = '#ccc';
            }
        });

        return listItem;
    }

    function createSidebar() {
        const sidebar = document.createElement('div');
        sidebar.id = 'claude-style-selector-sidebar';
        sidebar.style.cssText = `
            position: fixed;
            top: 0;
            right: 0;
            width: 280px;
            height: 100vh;
            background: #1a1a1a;
            border-left: 1px solid #333;
            padding: 20px;
            box-sizing: border-box;
            z-index: 1000;
            overflow-y: auto;
            font-family: system-ui, -apple-system, sans-serif;
        `;

        const header = document.createElement('div');
        header.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        `;

        const title = document.createElement('h3');
        title.textContent = 'Styles';
        title.style.cssText = `
            color: #fff;
            font-size: 16px;
            margin: 0;
            font-weight: 600;
        `;
        header.appendChild(title);

        const hideButton = document.createElement('button');
        hideButton.textContent = 'Hide';
        hideButton.style.cssText = `
            background: none;
            border: 1px solid #555;
            color: #ccc;
            padding: 2px 8px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 12px;
        `;
        hideButton.addEventListener('click', hideSidebar);
        header.appendChild(hideButton);

        sidebar.appendChild(header);

        // Create custom styles section first (numbers at top)
        if (customStyles.length > 0) {
            const customHeading = document.createElement('h4');
            customHeading.textContent = 'Custom';
            customHeading.style.cssText = `
                color: #fff;
                font-size: 12px;
                margin: 0 0 8px 0;
                font-weight: 500;
                text-transform: uppercase;
                letter-spacing: 0.5px;
            `;
            sidebar.appendChild(customHeading);

            const customStyleList = document.createElement('ul');
            customStyleList.style.cssText = `
                list-style: none;
                padding: 0;
                margin: 0 0 16px 0;
            `;

            for (let i = 0; i < customStyles.length; i++) {
                const styleName = customStyles[i];
                // Map index to key (0->1, 1->2, 2->3, 3->5, 4->6, etc.), skipping 4
                const indexToKey = ['1', '2', '3', '5', '6', '7', '8', '9'];
                const shortcut = i < indexToKey.length ? `Ctrl+Alt+${indexToKey[i]}` : null;
                const listItem = createStyleListItem(styleName, shortcut);
                customStyleList.appendChild(listItem);
            }

            sidebar.appendChild(customStyleList);
        }

        // Create separator if both sections exist
        if (customStyles.length > 0 && defaultStyles.length > 0) {
            const separator = document.createElement('hr');
            separator.style.cssText = `
                border: none;
                border-top: 1px solid #333;
                margin: 0 0 16px 0;
            `;
            sidebar.appendChild(separator);
        }

        // Create default styles section second (letters at bottom)
        if (defaultStyles.length > 0) {
            const defaultHeading = document.createElement('h4');
            defaultHeading.textContent = 'Default';
            defaultHeading.style.cssText = `
                color: #fff;
                font-size: 12px;
                margin: 0 0 8px 0;
                font-weight: 500;
                text-transform: uppercase;
                letter-spacing: 0.5px;
            `;
            sidebar.appendChild(defaultHeading);

            const defaultStyleList = document.createElement('ul');
            defaultStyleList.style.cssText = `
                list-style: none;
                padding: 0;
                margin: 0;
            `;

            for (let i = 0; i < defaultStyles.length; i++) {
                const styleName = defaultStyles[i];
                // Safe letters for default styles (avoiding E, U, I, O)
                const indexToKey = ['Q', 'W', 'R', 'T', 'Y', 'P'];
                const shortcut = i < indexToKey.length ? `Ctrl+Alt+${indexToKey[i]}` : null;
                const listItem = createStyleListItem(styleName, shortcut);
                defaultStyleList.appendChild(listItem);
            }

            sidebar.appendChild(defaultStyleList);
        }
        document.body.appendChild(sidebar);
        sidebarElement = sidebar;

        // Set initial selection state
        updateSidebarSelection();

        console.log('Claude Style Selector: Sidebar created with', defaultStyles.length, 'default styles and', customStyles.length, 'custom styles');
    }

    function createShowButton() {
        const button = document.createElement('button');
        button.id = 'claude-style-selector-show-button';
        button.textContent = 'Styles';
        button.style.cssText = `
            position: fixed;
            top: 50%;
            right: 0;
            transform: translateY(-50%) rotate(-90deg);
            transform-origin: bottom right;
            background: #1a1a1a;
            color: #fff;
            border: 1px solid #333;
            border-bottom: none;
            padding: 8px 16px;
            cursor: pointer;
            z-index: 999;
            display: none;
            font-family: system-ui, -apple-system, sans-serif;
            font-size: 14px;
            border-top-left-radius: 4px;
            border-top-right-radius: 4px;
        `;
        button.addEventListener('click', showSidebar);
        document.body.appendChild(button);
        showButtonElement = button;
    }

    // Start the fetch interceptor
    setupFetchInterceptor();
})();