Claude Style Selector v3.1.0
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();
})();