Last active
November 20, 2025 16:55
-
-
Save kiranwayne/e4d8846d2564d319aa500b6865d73407 to your computer and use it in GitHub Desktop.
Customize Text and Sidebar width, toggle justification, show/hide via menu on copilot.microsoft.com, add a Table of Contents
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Copilot Enhanced | |
| // @namespace https://gist.github.com/kiranwayne | |
| // @version 0.6.0 | |
| // @description Adds a Table of Contents, custom Text/Sidebar width, and justification toggle on copilot.microsoft.com. | |
| // @author kiranwayne | |
| // @match https://copilot.microsoft.com/* | |
| // @updateURL https://gist.githubusercontent.com/kiranwayne/e4d8846d2564d319aa500b6865d73407/raw/copilot_enhanced.js | |
| // @downloadURL https://gist.githubusercontent.com/kiranwayne/e4d8846d2564d319aa500b6865d73407/raw/copilot_enhanced.js | |
| // @grant GM_getValue | |
| // @grant GM_setValue | |
| // @grant GM_registerMenuCommand | |
| // @grant GM_unregisterMenuCommand | |
| // @run-at document-end | |
| // ==/UserScript== | |
| (async () => { | |
| 'use strict'; | |
| // --- Configuration & Constants --- | |
| const SCRIPT_NAME = 'Copilot Enhanced'; | |
| const SCRIPT_VERSION = '0.6.0'; | |
| const SCRIPT_AUTHOR = 'kiranwayne'; | |
| const CONFIG_PREFIX = 'copilotEnhancedControls_v4_'; // Incremented version | |
| // Text Settings | |
| const MAX_WIDTH_PX_KEY = CONFIG_PREFIX + 'textMaxWidthPx'; | |
| const USE_DEFAULT_TEXT_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultTextWidth'; | |
| const JUSTIFY_KEY = CONFIG_PREFIX + 'justifyEnabled'; | |
| // Sidebar Settings | |
| const SIDEBAR_WIDTH_PX_KEY = CONFIG_PREFIX + 'sidebarWidthPx'; | |
| const USE_DEFAULT_SIDEBAR_WIDTH_KEY = CONFIG_PREFIX + 'useDefaultSidebarWidth'; | |
| // UI Settings | |
| const SETTINGS_UI_VISIBLE_KEY = CONFIG_PREFIX + 'settingsUiVisible'; | |
| // ToC Settings (NEW) | |
| const TOC_VISIBLE_KEY = CONFIG_PREFIX + 'tocVisible'; | |
| const TOC_POSITION_KEY = CONFIG_PREFIX + 'tocPosition'; | |
| const TOC_COLLAPSED_KEY = CONFIG_PREFIX + 'tocCollapsed'; | |
| // Style & Element IDs | |
| const GLOBAL_STYLE_ID = 'vm-copilot-global-styles'; | |
| const TEXT_WIDTH_STYLE_ID = 'vm-copilot-text-width-style'; | |
| const JUSTIFY_STYLE_ID = 'vm-copilot-justify-style'; | |
| const SIDEBAR_STYLE_ID = 'vm-copilot-sidebar-style'; | |
| const SETTINGS_PANEL_ID = 'copilot-userscript-settings-panel'; | |
| const TOC_PANEL_ID = 'copilot-userscript-toc-panel'; // NEW | |
| // Text Width Config | |
| const SCRIPT_DEFAULT_TEXT_WIDTH_PX = 1000; | |
| const MIN_TEXT_WIDTH_PX = 500; | |
| const MAX_TEXT_WIDTH_PX = 2000; | |
| const STEP_TEXT_WIDTH_PX = 10; | |
| // Sidebar Width Config | |
| const SCRIPT_DEFAULT_SIDEBAR_WIDTH_PX = 500; | |
| const MIN_SIDEBAR_WIDTH_PX = 250; | |
| const MAX_SIDEBAR_WIDTH_PX = 1000; | |
| const STEP_SIDEBAR_WIDTH_PX = 5; | |
| const MINIMIZED_SIDEBAR_THRESHOLD_PX = 100; | |
| // Selectors | |
| const TEXT_CONTAINER_SELECTOR = '.max-w-chat'; | |
| const OUTER_SIDEBAR_SELECTOR = 'div.absolute.h-full.will-change-auto, div.relative.h-full.will-change-auto'; | |
| const INNER_SIDEBAR_SELECTOR = 'div.w-sidebar'; | |
| const MAIN_CONTENT_WRAPPER_SELECTOR = 'main.relative.w-full.min-w-0.will-change-auto'; | |
| const TOC_MESSAGE_SELECTOR = 'div[data-content="user-message"]'; // NEW | |
| // --- State Variables --- | |
| let config = {}; | |
| let settingsPanel = null; | |
| let textWidthSlider = null, textWidthLabel = null, textWidthInput = null, defaultTextWidthCheckbox = null, justifyCheckbox = null; | |
| let sidebarWidthSlider = null, sidebarWidthLabel = null, sidebarWidthInput = null, defaultSidebarWidthCheckbox = null; | |
| let menuCommandId_ToggleSettingsUI = null; | |
| let menuCommandId_ToggleTocUI = null; // NEW | |
| const allStyleRoots = new Set(); | |
| let sidebarObserver = null, tocObserver = null; // tocObserver is NEW | |
| let outerSidebarElement = null; | |
| let isObservingSidebar = false; | |
| let lastMessageCount = 0; // NEW | |
| let tocPanel = null; // NEW | |
| // --- Helper Functions --- | |
| async function loadSettings() { | |
| config.textMaxWidthPx = await GM_getValue(MAX_WIDTH_PX_KEY, SCRIPT_DEFAULT_TEXT_WIDTH_PX); | |
| config.useDefaultTextWidth = await GM_getValue(USE_DEFAULT_TEXT_WIDTH_KEY, false); | |
| config.justifyEnabled = await GM_getValue(JUSTIFY_KEY, false); | |
| config.sidebarWidthPx = await GM_getValue(SIDEBAR_WIDTH_PX_KEY, SCRIPT_DEFAULT_SIDEBAR_WIDTH_PX); | |
| config.useDefaultSidebarWidth = await GM_getValue(USE_DEFAULT_SIDEBAR_WIDTH_KEY, true); | |
| config.settingsUiVisible = await GM_getValue(SETTINGS_UI_VISIBLE_KEY, false); | |
| // NEW ToC settings | |
| config.tocVisible = await GM_getValue(TOC_VISIBLE_KEY, true); | |
| config.tocPosition = JSON.parse(await GM_getValue(TOC_POSITION_KEY, '{}')); | |
| config.tocCollapsed = await GM_getValue(TOC_COLLAPSED_KEY, false); | |
| } | |
| async function saveSetting(key, value) { | |
| // This function is now more generic and handles all config keys | |
| const configKey = key.replace(CONFIG_PREFIX, ''); | |
| config[configKey] = value; | |
| const valueToSave = (typeof value === 'object') ? JSON.stringify(value) : value; | |
| await GM_setValue(key, valueToSave); | |
| } | |
| // --- ToC Logic (NEW SECTION) --- | |
| function scrollToElement(element) { | |
| setTimeout(() => { | |
| element.scrollIntoView({ behavior: 'instant', block: 'start' }); | |
| }, 0); | |
| } | |
| function updateTocList() { | |
| if (!tocPanel) return; | |
| const tocList = tocPanel.querySelector('.toc-list'); | |
| if (!tocList) return; | |
| const userMessages = document.querySelectorAll(TOC_MESSAGE_SELECTOR); | |
| tocList.innerHTML = ''; | |
| userMessages.forEach((msg, index) => { | |
| const text = msg.textContent.trim(); | |
| if (!text) return; | |
| const snippet = text.substring(0, 50) + (text.length > 50 ? '...' : ''); | |
| const tocItem = document.createElement('div'); | |
| tocItem.textContent = `#${index + 1}: ${snippet}`; | |
| tocItem.title = text; | |
| tocItem.addEventListener('click', () => scrollToElement(msg)); | |
| tocList.appendChild(tocItem); | |
| }); | |
| lastMessageCount = userMessages.length; | |
| } | |
| // --- Style Generation Functions --- | |
| function getTextWidthCss() { return config.useDefaultTextWidth ? '' : `${TEXT_CONTAINER_SELECTOR} { max-width: ${config.textMaxWidthPx}px !important; }`; } | |
| function getJustifyCss() { return config.justifyEnabled ? `${TEXT_CONTAINER_SELECTOR} { text-align: justify !important; }` : ''; } | |
| function getSidebarCss() { | |
| if (config.useDefaultSidebarWidth) return ''; | |
| const sidebarWidthVar = `${config.sidebarWidthPx}px`; | |
| return ` | |
| ${INNER_SIDEBAR_SELECTOR} { width: ${sidebarWidthVar} !important; max-width: none !important; } | |
| ${MAIN_CONTENT_WRAPPER_SELECTOR}::before, ${MAIN_CONTENT_WRAPPER_SELECTOR}::after { left: ${sidebarWidthVar} !important; inset-inline-start: ${sidebarWidthVar} !important; pointer-events: none !important; } | |
| `; | |
| } | |
| // --- Style Injection --- | |
| function injectOrUpdateStyle(root, styleId, cssContent) { | |
| if (!root) return; | |
| const insertionPoint = (root.nodeType === Node.DOCUMENT_FRAGMENT_NODE) ? root : document.head; | |
| if (!insertionPoint) return; | |
| let style = insertionPoint.querySelector(`#${styleId}`); | |
| if (cssContent) { | |
| if (!style) { style = document.createElement('style'); style.id = styleId; style.textContent = cssContent; insertionPoint.appendChild(style); } | |
| else if (style.textContent !== cssContent) { style.textContent = cssContent; } | |
| } else { | |
| if (style) style.remove(); | |
| } | |
| } | |
| function injectGlobalStyles() { // Holds static styles for all UI panels | |
| const css = ` | |
| /* ToC Panel Styles */ | |
| #${TOC_PANEL_ID} { | |
| --bg-color: #202021; --text-color: #f0f0f0; --border-color: #3a3a3a; --hover-color: #4a4a4a; | |
| position: fixed; top: 40px; right: 40px; z-index: 9998; background-color: var(--bg-color); color: var(--text-color); | |
| padding: 10px 15px; border-radius: 8px; box-shadow: 0 8px 24px rgba(0,0,0,0.6); font-family: 'Segoe UI',Roboto,sans-serif; | |
| font-size: 14px; display: flex; flex-direction: column; gap: 8px; user-select: none; width: 320px; border: 1px solid var(--border-color); | |
| } | |
| #${TOC_PANEL_ID} .toc-title-bar { display: flex; justify-content: space-between; align-items: center; padding: 4px; text-align: center; cursor: grab; font-weight: 600; } | |
| #${TOC_PANEL_ID} .toc-toggle-btn { cursor: pointer; font-family: monospace; padding: 0 5px; } | |
| #${TOC_PANEL_ID} .toc-list { display: flex; flex-direction: column; gap: 4px; max-height: 280px; overflow-y: auto; padding: 2px; border-top: 1px solid var(--border-color); padding-top: 8px; } | |
| #${TOC_PANEL_ID} .toc-list div { padding: 5px 8px; cursor: pointer; border-radius: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: background-color 0.2s ease; border: 1px solid var(--border-color); } | |
| #${TOC_PANEL_ID} .toc-list div:hover { background-color: var(--hover-color); } | |
| #${TOC_PANEL_ID}.collapsed .toc-list { display: none; } | |
| `; | |
| const existingStyle = document.getElementById(GLOBAL_STYLE_ID); | |
| if (existingStyle) { existingStyle.textContent = css; } | |
| else { const styleSheet = document.createElement("style"); styleSheet.id = GLOBAL_STYLE_ID; styleSheet.innerText = css; document.head.appendChild(styleSheet); } | |
| } | |
| // --- Sidebar Observer Management --- | |
| const sidebarObserverConfig = { attributes: true, attributeFilter: ['style'] }; | |
| const sidebarObserverCallback = function(mutationsList, obs) { | |
| if (isObservingSidebar) { stopSidebarObserver(); applyStylesAndForceWidth(); } | |
| }; | |
| function startSidebarObserver() { | |
| if (!outerSidebarElement || config.useDefaultSidebarWidth || isObservingSidebar) { if(config.useDefaultSidebarWidth) stopSidebarObserver(); return; } | |
| if (!sidebarObserver) { sidebarObserver = new MutationObserver(sidebarObserverCallback); } | |
| try { sidebarObserver.takeRecords(); sidebarObserver.observe(outerSidebarElement, sidebarObserverConfig); isObservingSidebar = true; } | |
| catch (e) { console.error('[Copilot Enhanced] Error starting MutationObserver:', e); isObservingSidebar = false; } | |
| } | |
| function stopSidebarObserver() { | |
| if (sidebarObserver && isObservingSidebar) { | |
| try { sidebarObserver.disconnect(); isObservingSidebar = false; } | |
| catch (e) { console.error('[Copilot Enhanced] Error stopping MutationObserver:', e); } | |
| } | |
| } | |
| // --- Global Style Application & Forcing --- | |
| function applyStylesAndForceWidth() { | |
| const textWidthCss = getTextWidthCss(); | |
| const justifyCss = getJustifyCss(); | |
| allStyleRoots.forEach(root => { | |
| if (root) { | |
| injectOrUpdateStyle(root, TEXT_WIDTH_STYLE_ID, textWidthCss); | |
| injectOrUpdateStyle(root, JUSTIFY_STYLE_ID, justifyCss); | |
| } | |
| }); | |
| const isCustomSidebarEnabled = !config.useDefaultSidebarWidth; | |
| let sidebarCssToInject = ''; | |
| let isSidebarMinimized = false; | |
| if (outerSidebarElement && isCustomSidebarEnabled) { | |
| const currentWidthPx = parseFloat(outerSidebarElement.style.width); | |
| if (!isNaN(currentWidthPx) && currentWidthPx > 0 && currentWidthPx < MINIMIZED_SIDEBAR_THRESHOLD_PX) { | |
| isSidebarMinimized = true; | |
| } | |
| } | |
| if (isCustomSidebarEnabled && !isSidebarMinimized) { | |
| sidebarCssToInject = getSidebarCss(); | |
| } | |
| allStyleRoots.forEach(root => { | |
| if (root) { | |
| injectOrUpdateStyle(root, SIDEBAR_STYLE_ID, sidebarCssToInject); | |
| } | |
| }); | |
| if (outerSidebarElement) { | |
| if (isCustomSidebarEnabled && !isSidebarMinimized) { | |
| const widthToForce = `${config.sidebarWidthPx}px`; | |
| if (outerSidebarElement.style.getPropertyValue('width') !== widthToForce || outerSidebarElement.style.getPropertyPriority('width') !== 'important') { | |
| outerSidebarElement.style.setProperty('width', widthToForce, 'important'); | |
| outerSidebarElement.style.setProperty('max-width', widthToForce, 'important'); | |
| } | |
| } else { | |
| if (outerSidebarElement.style.getPropertyPriority('width') === 'important') { | |
| outerSidebarElement.style.removeProperty('width'); | |
| } | |
| if (outerSidebarElement.style.getPropertyPriority('max-width') === 'important') { | |
| outerSidebarElement.style.removeProperty('max-width'); | |
| } | |
| } | |
| } | |
| if (isCustomSidebarEnabled) { | |
| startSidebarObserver(); | |
| } else { | |
| stopSidebarObserver(); | |
| } | |
| } | |
| // --- Click Outside Handler for Settings Panel --- | |
| async function handleClickOutside(event) { if (settingsPanel && document.body.contains(settingsPanel) && !settingsPanel.contains(event.target)) { await saveSetting(SETTINGS_UI_VISIBLE_KEY, false); removeSettingsUI(); updateTampermonkeyMenu(); } } | |
| // --- UI Creation/Removal --- | |
| function removeSettingsUI() { document.removeEventListener('click', handleClickOutside, true); settingsPanel = document.getElementById(SETTINGS_PANEL_ID); if (settingsPanel) { settingsPanel.remove(); settingsPanel = null; textWidthSlider = textWidthLabel = textWidthInput = defaultTextWidthCheckbox = justifyCheckbox = null; sidebarWidthSlider = sidebarWidthLabel = sidebarWidthInput = defaultSidebarWidthCheckbox = null; } } | |
| function createSettingsUI() { | |
| if (document.getElementById(SETTINGS_PANEL_ID) || !config.settingsUiVisible) return; | |
| removeSettingsUI(); | |
| settingsPanel = document.createElement('div'); | |
| settingsPanel.id = SETTINGS_PANEL_ID; | |
| Object.assign(settingsPanel.style, { position: 'fixed', top: '10px', right: '10px', zIndex: '9999', display: 'block', background: '#343541', color: '#ECECF1', border: '1px solid #565869', borderRadius: '6px', padding: '15px', boxShadow: '0 4px 10px rgba(0,0,0,0.3)', minWidth: '280px' }); | |
| const headerDiv = document.createElement('div'); headerDiv.style.marginBottom = '10px'; headerDiv.style.paddingBottom = '10px'; headerDiv.style.borderBottom = '1px solid #565869'; const titleElement = document.createElement('h4'); titleElement.textContent = SCRIPT_NAME; Object.assign(titleElement.style, { margin: '0 0 5px 0', fontSize: '1.1em', fontWeight: 'bold', color: '#FFFFFF'}); const versionElement = document.createElement('p'); versionElement.textContent = `Version: ${SCRIPT_VERSION}`; Object.assign(versionElement.style, { margin: '0 0 2px 0', fontSize: '0.85em', opacity: '0.8'}); const authorElement = document.createElement('p'); authorElement.textContent = `Author: ${SCRIPT_AUTHOR}`; Object.assign(authorElement.style, { margin: '0', fontSize: '0.85em', opacity: '0.8'}); headerDiv.appendChild(titleElement); headerDiv.appendChild(versionElement); headerDiv.appendChild(authorElement); settingsPanel.appendChild(headerDiv); | |
| const textSection = document.createElement('div'); textSection.style.marginTop = '10px'; const textHeader = document.createElement('h5'); textHeader.textContent = 'Text'; Object.assign(textHeader.style, { margin: '10px 0 5px 0', fontSize: '1em', fontWeight: 'bold', color: '#FFFFFF'}); textSection.appendChild(textHeader); const defaultTextWidthDiv = document.createElement('div'); defaultTextWidthDiv.style.marginBottom = '10px'; defaultTextWidthCheckbox = document.createElement('input'); defaultTextWidthCheckbox.type = 'checkbox'; defaultTextWidthCheckbox.id = 'copilot-userscript-default-text-width-toggle'; const defaultTextWidthLabel = document.createElement('label'); defaultTextWidthLabel.htmlFor = defaultTextWidthCheckbox.id; defaultTextWidthLabel.textContent = ' Use Default Text Width'; defaultTextWidthLabel.style.cursor = 'pointer'; defaultTextWidthDiv.appendChild(defaultTextWidthCheckbox); defaultTextWidthDiv.appendChild(defaultTextWidthLabel); textSection.appendChild(defaultTextWidthDiv); const customTextWidthControlsDiv = document.createElement('div'); customTextWidthControlsDiv.style.display = 'flex'; customTextWidthControlsDiv.style.alignItems = 'center'; customTextWidthControlsDiv.style.gap = '10px'; textWidthLabel = document.createElement('span'); textWidthLabel.style.minWidth = '50px'; textWidthLabel.style.fontFamily = 'monospace'; textWidthLabel.style.textAlign = 'right'; textWidthSlider = document.createElement('input'); textWidthSlider.type = 'range'; textWidthSlider.min = MIN_TEXT_WIDTH_PX; textWidthSlider.max = MAX_TEXT_WIDTH_PX; textWidthSlider.step = STEP_TEXT_WIDTH_PX; textWidthSlider.style.flexGrow = '1'; textWidthInput = document.createElement('input'); textWidthInput.type = 'number'; textWidthInput.min = MIN_TEXT_WIDTH_PX; textWidthInput.max = MAX_TEXT_WIDTH_PX; textWidthInput.step = STEP_TEXT_WIDTH_PX; Object.assign(textWidthInput.style, { width: '60px', padding: '2px 4px', background: '#202123', color: '#ECECF1', border: '1px solid #565869', borderRadius: '4px' }); customTextWidthControlsDiv.appendChild(textWidthLabel); customTextWidthControlsDiv.appendChild(textWidthSlider); customTextWidthControlsDiv.appendChild(textWidthInput); textSection.appendChild(customTextWidthControlsDiv); const justifyDiv = document.createElement('div'); justifyDiv.style.marginTop = '10px'; justifyCheckbox = document.createElement('input'); justifyCheckbox.type = 'checkbox'; justifyCheckbox.id = 'copilot-userscript-justify-toggle'; const justifyLabel = document.createElement('label'); justifyLabel.htmlFor = justifyCheckbox.id; justifyLabel.textContent = ' Enable Text Justification'; justifyLabel.style.cursor = 'pointer'; justifyDiv.appendChild(justifyCheckbox); justifyDiv.appendChild(justifyLabel); textSection.appendChild(justifyDiv); settingsPanel.appendChild(textSection); | |
| const sidebarSection = document.createElement('div'); sidebarSection.style.borderTop = '1px solid #565869'; sidebarSection.style.paddingTop = '15px'; sidebarSection.style.marginTop = '15px'; const sidebarHeader = document.createElement('h5'); sidebarHeader.textContent = 'Sidebar'; Object.assign(sidebarHeader.style, { margin: '0 0 5px 0', fontSize: '1em', fontWeight: 'bold', color: '#FFFFFF'}); sidebarSection.appendChild(sidebarHeader); const defaultSidebarWidthDiv = document.createElement('div'); defaultSidebarWidthDiv.style.marginBottom = '10px'; defaultSidebarWidthCheckbox = document.createElement('input'); defaultSidebarWidthCheckbox.type = 'checkbox'; defaultSidebarWidthCheckbox.id = 'copilot-userscript-default-sidebar-width-toggle'; const defaultSidebarWidthLabel = document.createElement('label'); defaultSidebarWidthLabel.htmlFor = defaultSidebarWidthCheckbox.id; defaultSidebarWidthLabel.textContent = ' Use Default Sidebar Width'; defaultSidebarWidthLabel.style.cursor = 'pointer'; defaultSidebarWidthDiv.appendChild(defaultSidebarWidthCheckbox); defaultSidebarWidthDiv.appendChild(defaultSidebarWidthLabel); sidebarSection.appendChild(defaultSidebarWidthDiv); const customSidebarWidthControlsDiv = document.createElement('div'); customSidebarWidthControlsDiv.style.display = 'flex'; customSidebarWidthControlsDiv.style.alignItems = 'center'; customSidebarWidthControlsDiv.style.gap = '10px'; sidebarWidthLabel = document.createElement('span'); sidebarWidthLabel.style.minWidth = '50px'; sidebarWidthLabel.style.fontFamily = 'monospace'; sidebarWidthLabel.style.textAlign = 'right'; sidebarWidthSlider = document.createElement('input'); sidebarWidthSlider.type = 'range'; sidebarWidthSlider.min = MIN_SIDEBAR_WIDTH_PX; sidebarWidthSlider.max = MAX_SIDEBAR_WIDTH_PX; sidebarWidthSlider.step = STEP_SIDEBAR_WIDTH_PX; sidebarWidthSlider.style.flexGrow = '1'; sidebarWidthInput = document.createElement('input'); sidebarWidthInput.type = 'number'; sidebarWidthInput.min = MIN_SIDEBAR_WIDTH_PX; sidebarWidthInput.max = MAX_SIDEBAR_WIDTH_PX; sidebarWidthInput.step = STEP_SIDEBAR_WIDTH_PX; Object.assign(sidebarWidthInput.style, { width: '60px', padding: '2px 4px', background: '#202123', color: '#ECECF1', border: '1px solid #565869', borderRadius: '4px' }); customSidebarWidthControlsDiv.appendChild(sidebarWidthLabel); customSidebarWidthControlsDiv.appendChild(sidebarWidthSlider); customSidebarWidthControlsDiv.appendChild(sidebarWidthInput); sidebarSection.appendChild(customSidebarWidthControlsDiv); settingsPanel.appendChild(sidebarSection); document.body.appendChild(settingsPanel); | |
| defaultTextWidthCheckbox.addEventListener('change', async (e) => { await saveSetting(USE_DEFAULT_TEXT_WIDTH_KEY, e.target.checked); applyStylesAndForceWidth(); updateSettingsUIState(); }); | |
| textWidthSlider.addEventListener('input', (e) => { config.textMaxWidthPx = parseInt(e.target.value, 10); textWidthInput.value = config.textMaxWidthPx; updateSettingsUIState(); if (!config.useDefaultTextWidth) applyStylesAndForceWidth(); }); | |
| textWidthSlider.addEventListener('change', async (e) => { if (!config.useDefaultTextWidth) await saveSetting(MAX_WIDTH_PX_KEY, parseInt(e.target.value, 10)); }); | |
| textWidthInput.addEventListener('input', (e) => { let numVal = parseInt(e.target.value, 10); if (!isNaN(numVal) || e.target.value === '' || e.target.value === '-') { let tempVal = isNaN(numVal) ? MIN_TEXT_WIDTH_PX : numVal; textWidthSlider.value = tempVal; textWidthLabel.textContent = `${tempVal}px`; if (!config.useDefaultTextWidth) { let clampedForStyle = Math.max(MIN_TEXT_WIDTH_PX, Math.min(MAX_TEXT_WIDTH_PX, tempVal)); config.textMaxWidthPx = clampedForStyle; applyStylesAndForceWidth(); } } }); | |
| textWidthInput.addEventListener('change', async (e) => { let finalVal = parseInt(e.target.value, 10); finalVal = isNaN(finalVal) ? SCRIPT_DEFAULT_TEXT_WIDTH_PX : Math.max(MIN_TEXT_WIDTH_PX, Math.min(MAX_TEXT_WIDTH_PX, finalVal)); config.textMaxWidthPx = finalVal; e.target.value = finalVal; await saveSetting(MAX_WIDTH_PX_KEY, finalVal); updateSettingsUIState(); if (!config.useDefaultTextWidth) { applyStylesAndForceWidth(); } }); | |
| justifyCheckbox.addEventListener('change', async (e) => { await saveSetting(JUSTIFY_KEY, e.target.checked); applyStylesAndForceWidth(); }); | |
| defaultSidebarWidthCheckbox.addEventListener('change', async (e) => { await saveSetting(USE_DEFAULT_SIDEBAR_WIDTH_KEY, e.target.checked); applyStylesAndForceWidth(); updateSettingsUIState(); }); | |
| sidebarWidthSlider.addEventListener('input', (e) => { config.sidebarWidthPx = parseInt(e.target.value, 10); sidebarWidthInput.value = config.sidebarWidthPx; updateSettingsUIState(); if (!config.useDefaultSidebarWidth) applyStylesAndForceWidth(); }); | |
| sidebarWidthSlider.addEventListener('change', async (e) => { if (!config.useDefaultSidebarWidth) await saveSetting(SIDEBAR_WIDTH_PX_KEY, parseInt(e.target.value, 10)); }); | |
| sidebarWidthInput.addEventListener('input', (e) => { let numVal = parseInt(e.target.value, 10); if (!isNaN(numVal) || e.target.value === '' || e.target.value === '-') { let tempVal = isNaN(numVal) ? MIN_SIDEBAR_WIDTH_PX : numVal; sidebarWidthSlider.value = tempVal; sidebarWidthLabel.textContent = `${tempVal}px`; if (!config.useDefaultSidebarWidth) { let clampedForStyle = Math.max(MIN_SIDEBAR_WIDTH_PX, Math.min(MAX_SIDEBAR_WIDTH_PX, tempVal)); config.sidebarWidthPx = clampedForStyle; applyStylesAndForceWidth(); } } }); | |
| sidebarWidthInput.addEventListener('change', async (e) => { let finalVal = parseInt(e.target.value, 10); finalVal = isNaN(finalVal) ? SCRIPT_DEFAULT_SIDEBAR_WIDTH_PX : Math.max(MIN_SIDEBAR_WIDTH_PX, Math.min(MAX_SIDEBAR_WIDTH_PX, finalVal)); config.sidebarWidthPx = finalVal; e.target.value = finalVal; await saveSetting(SIDEBAR_WIDTH_PX_KEY, finalVal); updateSettingsUIState(); if (!config.useDefaultSidebarWidth) { applyStylesAndForceWidth(); } }); | |
| updateSettingsUIState(); | |
| textWidthInput.value = config.textMaxWidthPx; | |
| sidebarWidthInput.value = config.sidebarWidthPx; | |
| document.addEventListener('click', handleClickOutside, true); | |
| } | |
| function removeTocUI() { if (tocPanel) { tocPanel.remove(); tocPanel = null; } } // NEW | |
| function createOrUpdateTocUI() { // NEW | |
| removeTocUI(); | |
| if (!config.tocVisible) return; | |
| tocPanel = document.createElement('div'); | |
| tocPanel.id = TOC_PANEL_ID; | |
| Object.assign(tocPanel.style, { | |
| top: config.tocPosition.top || '40px', | |
| left: config.tocPosition.left || 'auto', | |
| right: config.tocPosition.left ? 'auto' : (config.tocPosition.right || '40px') | |
| }); | |
| const titleBar = document.createElement('div'); | |
| titleBar.className = 'toc-title-bar'; | |
| const titleText = document.createElement('span'); | |
| titleText.textContent = 'User Messages (drag me)'; | |
| const toggleBtn = document.createElement('span'); | |
| toggleBtn.className = 'toc-toggle-btn'; | |
| titleBar.append(titleText, toggleBtn); | |
| const tocList = document.createElement('div'); | |
| tocList.className = 'toc-list'; | |
| tocPanel.append(titleBar, tocList); | |
| document.body.appendChild(tocPanel); | |
| toggleBtn.addEventListener('click', async (e) => { | |
| e.stopPropagation(); | |
| await saveSetting(TOC_COLLAPSED_KEY, !config.tocCollapsed); | |
| updateTocUIState(); | |
| }); | |
| titleBar.addEventListener('mousedown', function(e) { | |
| e.preventDefault(); titleBar.style.cursor = 'grabbing'; const initialX = e.clientX, initialY = e.clientY, initialTop = tocPanel.offsetTop, initialLeft = tocPanel.offsetLeft; | |
| function move(e) { tocPanel.style.top = `${initialTop + e.clientY - initialY}px`; tocPanel.style.left = `${initialLeft + e.clientX - initialX}px`; tocPanel.style.right = 'auto'; } | |
| function stop() { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', stop); titleBar.style.cursor = 'grab'; saveSetting(TOC_POSITION_KEY, { top: tocPanel.style.top, left: tocPanel.style.left }); } | |
| document.addEventListener('mousemove', move); document.addEventListener('mouseup', stop); | |
| }); | |
| updateTocUIState(); | |
| updateTocList(); | |
| } | |
| // --- UI State Update --- | |
| function updateSettingsUIState() { | |
| if (!settingsPanel) return; | |
| if (defaultTextWidthCheckbox) defaultTextWidthCheckbox.checked = config.useDefaultTextWidth; | |
| const isCustomTextWidthEnabled = !config.useDefaultTextWidth; | |
| if (textWidthSlider) textWidthSlider.disabled = !isCustomTextWidthEnabled; | |
| if (textWidthInput) textWidthInput.disabled = !isCustomTextWidthEnabled; | |
| if (textWidthLabel) textWidthLabel.style.opacity = isCustomTextWidthEnabled ? 1 : 0.5; | |
| if (textWidthSlider) textWidthSlider.style.opacity = isCustomTextWidthEnabled ? 1 : 0.5; | |
| if (textWidthInput) textWidthInput.style.opacity = isCustomTextWidthEnabled ? 1 : 0.5; | |
| if (textWidthSlider) textWidthSlider.value = config.textMaxWidthPx; | |
| if (textWidthLabel) textWidthLabel.textContent = `${config.textMaxWidthPx}px`; | |
| if (justifyCheckbox) justifyCheckbox.checked = config.justifyEnabled; | |
| if (defaultSidebarWidthCheckbox) defaultSidebarWidthCheckbox.checked = config.useDefaultSidebarWidth; | |
| const isCustomSidebarWidthEnabled = !config.useDefaultSidebarWidth; | |
| if (sidebarWidthSlider) sidebarWidthSlider.disabled = !isCustomSidebarWidthEnabled; | |
| if (sidebarWidthInput) sidebarWidthInput.disabled = !isCustomSidebarWidthEnabled; | |
| if (sidebarWidthLabel) sidebarWidthLabel.style.opacity = isCustomSidebarWidthEnabled ? 1 : 0.5; | |
| if (sidebarWidthSlider) sidebarWidthSlider.style.opacity = isCustomSidebarWidthEnabled ? 1 : 0.5; | |
| if (sidebarWidthInput) sidebarWidthInput.style.opacity = isCustomSidebarWidthEnabled ? 1 : 0.5; | |
| if (sidebarWidthSlider) sidebarWidthSlider.value = config.sidebarWidthPx; | |
| if (sidebarWidthLabel) sidebarWidthLabel.textContent = `${config.sidebarWidthPx}px`; | |
| } | |
| function updateTocUIState() { // NEW | |
| if (!tocPanel) return; | |
| const toggleBtn = tocPanel.querySelector('.toc-toggle-btn'); | |
| if (config.tocCollapsed) { | |
| tocPanel.classList.add('collapsed'); | |
| if(toggleBtn) toggleBtn.textContent = '[+]'; | |
| } else { | |
| tocPanel.classList.remove('collapsed'); | |
| if(toggleBtn) toggleBtn.textContent = '[-]'; | |
| } | |
| } | |
| // --- Tampermonkey Menu --- | |
| function updateTampermonkeyMenu() { | |
| if (menuCommandId_ToggleSettingsUI) try { GM_unregisterMenuCommand(menuCommandId_ToggleSettingsUI); } catch(e) {} | |
| if (menuCommandId_ToggleTocUI) try { GM_unregisterMenuCommand(menuCommandId_ToggleTocUI); } catch(e) {} | |
| const settingsLabel = config.settingsUiVisible ? 'Hide Settings Panel' : 'Show Settings Panel'; | |
| menuCommandId_ToggleSettingsUI = GM_registerMenuCommand(settingsLabel, async () => { | |
| await saveSetting(SETTINGS_UI_VISIBLE_KEY, !config.settingsUiVisible); | |
| if (config.settingsUiVisible) createSettingsUI(); else removeSettingsUI(); | |
| updateTampermonkeyMenu(); | |
| }); | |
| const tocLabel = config.tocVisible ? 'Hide Table of Contents' : 'Show Table of Contents'; | |
| menuCommandId_ToggleTocUI = GM_registerMenuCommand(tocLabel, async () => { | |
| await saveSetting(TOC_VISIBLE_KEY, !config.tocVisible); | |
| createOrUpdateTocUI(); | |
| updateTampermonkeyMenu(); | |
| }); | |
| } | |
| // --- Shadow DOM & Initialization --- | |
| function getShadowRoot(element) { try { return element.shadowRoot; } catch (e) { return null; } } | |
| function processElement(element) { | |
| const shadow = getShadowRoot(element); | |
| if (shadow && shadow.nodeType === Node.DOCUMENT_FRAGMENT_NODE && !allStyleRoots.has(shadow)) { | |
| allStyleRoots.add(shadow); | |
| injectOrUpdateStyle(shadow, TEXT_WIDTH_STYLE_ID, getTextWidthCss()); | |
| injectOrUpdateStyle(shadow, JUSTIFY_STYLE_ID, getJustifyCss()); | |
| injectOrUpdateStyle(shadow, SIDEBAR_STYLE_ID, getSidebarCss()); | |
| return true; | |
| } | |
| return false; | |
| } | |
| function waitForSidebarElement() { | |
| const checkInterval = 500; const waitTimeout = 15000; let timeWaited = 0; | |
| const intervalId = setInterval(() => { | |
| const targetNode = document.querySelector(OUTER_SIDEBAR_SELECTOR); | |
| if (targetNode) { | |
| clearInterval(intervalId); | |
| outerSidebarElement = targetNode; | |
| applyStylesAndForceWidth(); | |
| } else { | |
| timeWaited += checkInterval; | |
| if (timeWaited >= waitTimeout) { | |
| clearInterval(intervalId); | |
| console.error(`[Copilot Enhanced] Outer sidebar element NOT FOUND. Selector: ${OUTER_SIDEBAR_SELECTOR}`); | |
| applyStylesAndForceWidth(); | |
| } | |
| } | |
| }, checkInterval); | |
| } | |
| async function main() { | |
| console.log(`[${SCRIPT_NAME} v${SCRIPT_VERSION}] Initializing...`); | |
| if (document.head) allStyleRoots.add(document.head); | |
| await loadSettings(); | |
| injectGlobalStyles(); | |
| waitForSidebarElement(); | |
| document.querySelectorAll('*').forEach(el => processElement(el)); | |
| if (config.settingsUiVisible) createSettingsUI(); | |
| createOrUpdateTocUI(); | |
| updateTampermonkeyMenu(); | |
| const shadowDomObserver = new MutationObserver((mutations) => { mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { processElement(node); try { node.querySelectorAll('*').forEach(el => processElement(el)); } catch(e) {} } }); }); }); | |
| shadowDomObserver.observe(document.documentElement, { childList: true, subtree: true }); | |
| tocObserver = new MutationObserver(() => { | |
| const currentMessageCount = document.querySelectorAll(TOC_MESSAGE_SELECTOR).length; | |
| if (currentMessageCount !== lastMessageCount) { | |
| updateTocList(); | |
| } | |
| }); | |
| tocObserver.observe(document.documentElement, { childList: true, subtree: true }); | |
| window.addEventListener('beforeunload', () => { | |
| stopSidebarObserver(); | |
| if (shadowDomObserver) shadowDomObserver.disconnect(); | |
| if (tocObserver) tocObserver.disconnect(); | |
| console.log('[Copilot Enhanced] Observers disconnected on unload.'); | |
| }); | |
| console.log(`[${SCRIPT_NAME}] Initialization complete.`); | |
| } | |
| main(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The Copilot website have pushed a UI update or something that makes Copilot Enhancer no longer actually expand the Sidebar width.