Skip to content

Instantly share code, notes, and snippets.

@kiranwayne
Last active November 20, 2025 16:55
Show Gist options
  • Select an option

  • Save kiranwayne/e4d8846d2564d319aa500b6865d73407 to your computer and use it in GitHub Desktop.

Select an option

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
// ==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();
})();
@kiranwayne
Copy link
Author

@sliceanddice9 Please try v0.5.2 with the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment