Last active
June 29, 2026 01:06
-
-
Save kiranwayne/0eb8395422892f5768c973e8c026c9aa to your computer and use it in GitHub Desktop.
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 M365 Copilot++ | |
| // @namespace https://m365.cloud.microsoft/ | |
| // @version 1.0.0 | |
| // @description Adds a draggable table of contents for user messages and a max-width slider to M365 Copilot chat pages. | |
| // @author Converted from bookmarklet | |
| // @match https://m365.cloud.microsoft/* | |
| // @match https://www.microsoft365.com/* | |
| // @match https://copilot.microsoft.com/* | |
| // @updateURL https://gist.github.com/kiranwayne/0eb8395422892f5768c973e8c026c9aa/raw/M365_Copilot++.js | |
| // @downloadURL https://gist.github.com/kiranwayne/0eb8395422892f5768c973e8c026c9aa/raw/M365_Copilot++.js | |
| // @grant none | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const CONTAINER_ID = 'm365-bookmarklet-container'; | |
| const STYLE_ID = 'm365-bookmarklet-styles'; | |
| const WIDTH_STYLE_ID = 'm365-bookmarklet-width-style'; | |
| const MSG_SELECTOR = 'div[data-testid="chatQuestion"]'; | |
| const MSG_TEXT_SELECTOR = 'div[data-testid="chatOutput"]'; | |
| const STORAGE_KEY_WIDTH = 'm365BookmarkletWidth'; | |
| const DEFAULT_WIDTH = 1300; | |
| const MIN_WIDTH = 600; | |
| const MAX_WIDTH = 3000; | |
| if (document.getElementById(CONTAINER_ID)) { | |
| return; | |
| } | |
| let lastMessageCount = 0; | |
| let lastUrl = location.href; | |
| let navTimeout; | |
| let tocUpdateTimeout; | |
| let container; | |
| function injectGlobalStyles() { | |
| if (document.getElementById(STYLE_ID)) return; | |
| const css = ` | |
| #${CONTAINER_ID}{--bg-color:#202021;--text-color:#f0f0f0;--border-color:#3a3a3a;--accent-color:#3b82f6;--hover-color:#4a4a4a;--track-color:#555;position:fixed;bottom:15px;right:15px;z-index:99999;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)} | |
| #${CONTAINER_ID} .title-bar{padding:4px;text-align:center;cursor:grab;font-weight:600} | |
| #${CONTAINER_ID} .control-group{display:flex;align-items:center;gap:12px;padding:4px 0} | |
| #${CONTAINER_ID} label{white-space:nowrap;font-weight:600} | |
| #${CONTAINER_ID} .value-label{min-width:50px;text-align:right;font-family:monospace} | |
| #${CONTAINER_ID} .toc-header{font-weight:600;margin-top:8px;padding-bottom:8px;border-bottom:1px solid var(--border-color)} | |
| #${CONTAINER_ID} .toc-list{display:flex;flex-direction:column;gap:4px;max-height:280px;overflow-y:auto;padding:2px} | |
| #${CONTAINER_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);flex-shrink:0;min-height:30px;box-sizing:border-box;line-height:20px} | |
| #${CONTAINER_ID} .toc-list div:hover{background-color:var(--hover-color)} | |
| #${CONTAINER_ID} .toc-list::-webkit-scrollbar{width:6px} | |
| #${CONTAINER_ID} .toc-list::-webkit-scrollbar-track{background:transparent} | |
| #${CONTAINER_ID} .toc-list::-webkit-scrollbar-thumb{background-color:var(--border-color);border-radius:6px} | |
| #${CONTAINER_ID} input[type=range]{-webkit-appearance:none;width:100%;background:transparent} | |
| #${CONTAINER_ID} input[type=range]::-webkit-slider-runnable-track{width:100%;height:4px;cursor:pointer;border-radius:4px} | |
| #${CONTAINER_ID} input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;height:16px;width:16px;border-radius:50%;background:var(--accent-color);cursor:pointer;margin-top:-6px;border:none} | |
| #${CONTAINER_ID} input[type=range]::-moz-range-track{width:100%;height:4px;cursor:pointer;border-radius:4px} | |
| #${CONTAINER_ID} input[type=range]::-moz-range-thumb{height:16px;width:16px;border-radius:50%;background:var(--accent-color);cursor:pointer;border:none} | |
| `; | |
| const styleSheet = document.createElement('style'); | |
| styleSheet.id = STYLE_ID; | |
| styleSheet.textContent = css; | |
| document.head.appendChild(styleSheet); | |
| } | |
| function updateWidth(newWidth) { | |
| let style = document.getElementById(WIDTH_STYLE_ID); | |
| if (!style) { | |
| style = document.createElement('style'); | |
| style.id = WIDTH_STYLE_ID; | |
| document.head.appendChild(style); | |
| } | |
| const w = `${newWidth}px`; | |
| style.textContent = ` | |
| .m365-chat-llm-web-ui-chat-chat-message, | |
| .fai-UserMessage, | |
| .fai-CopilotMessage, | |
| [data-testid="copilot-message-div"], | |
| [data-testid="copilot-message-reply-div"], | |
| .fai-CopilotMessage__content{max-width:${w}!important} | |
| .m365-chat-llm-web-ui-chat-chat-message *{max-width:none!important} | |
| `; | |
| const valueLabel = document.querySelector(`#${CONTAINER_ID} .value-label`); | |
| if (valueLabel) valueLabel.textContent = w; | |
| localStorage.setItem(STORAGE_KEY_WIDTH, String(newWidth)); | |
| } | |
| function updateSliderStyle(slider) { | |
| const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100; | |
| slider.style.background = `linear-gradient(to right,var(--accent-color) ${percentage}%,var(--track-color) ${percentage}%)`; | |
| } | |
| function scrollToElement(element) { | |
| setTimeout(() => { | |
| element.scrollIntoView({ behavior: 'instant', block: 'start' }); | |
| }, 0); | |
| } | |
| function resetPosition() { | |
| if (!container) return; | |
| container.style.top = ''; | |
| container.style.left = ''; | |
| container.style.bottom = '15px'; | |
| container.style.right = '15px'; | |
| } | |
| function handleNavigation() { | |
| if (location.href === lastUrl) return; | |
| lastUrl = location.href; | |
| resetPosition(); | |
| lastMessageCount = -1; | |
| clearTimeout(navTimeout); | |
| updateToc(); | |
| navTimeout = setTimeout(updateToc, 1500); | |
| } | |
| function updateToc() { | |
| const tocList = document.querySelector(`#${CONTAINER_ID} .toc-list`); | |
| if (!tocList) return; | |
| const userMessages = document.querySelectorAll(MSG_SELECTOR); | |
| tocList.innerHTML = ''; | |
| userMessages.forEach((msg, index) => { | |
| const textEl = msg.querySelector(MSG_TEXT_SELECTOR); | |
| const text = textEl ? textEl.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); | |
| }); | |
| const count = tocList.children.length; | |
| const desired = Math.max(140, Math.min(count * 34, 500)); | |
| tocList.style.maxHeight = `${desired}px`; | |
| lastMessageCount = userMessages.length; | |
| } | |
| function makeDraggable(titleBar) { | |
| titleBar.addEventListener('mousedown', function (event) { | |
| event.preventDefault(); | |
| titleBar.style.cursor = 'grabbing'; | |
| const startX = event.clientX; | |
| const startY = event.clientY; | |
| const rect = container.getBoundingClientRect(); | |
| const startTop = rect.top; | |
| const startLeft = rect.left; | |
| container.style.top = `${startTop}px`; | |
| container.style.left = `${startLeft}px`; | |
| container.style.bottom = 'auto'; | |
| container.style.right = 'auto'; | |
| function move(moveEvent) { | |
| const newTop = startTop + moveEvent.clientY - startY; | |
| const newLeft = startLeft + moveEvent.clientX - startX; | |
| const containerWidth = container.offsetWidth; | |
| const containerHeight = container.offsetHeight; | |
| container.style.top = `${Math.max(0, Math.min(newTop, window.innerHeight - containerHeight))}px`; | |
| container.style.left = `${Math.max(0, Math.min(newLeft, window.innerWidth - containerWidth))}px`; | |
| } | |
| function stop() { | |
| document.removeEventListener('mousemove', move); | |
| document.removeEventListener('mouseup', stop); | |
| titleBar.style.cursor = 'grab'; | |
| } | |
| document.addEventListener('mousemove', move); | |
| document.addEventListener('mouseup', stop); | |
| }); | |
| } | |
| function createUi() { | |
| injectGlobalStyles(); | |
| container = document.createElement('div'); | |
| container.id = CONTAINER_ID; | |
| const titleBar = document.createElement('div'); | |
| titleBar.className = 'title-bar'; | |
| titleBar.textContent = 'TOC & Width (drag me)'; | |
| const widthContainer = document.createElement('div'); | |
| widthContainer.className = 'control-group'; | |
| const label = document.createElement('label'); | |
| label.textContent = 'Max Width:'; | |
| const slider = document.createElement('input'); | |
| slider.type = 'range'; | |
| slider.min = String(MIN_WIDTH); | |
| slider.max = String(MAX_WIDTH); | |
| slider.step = '10'; | |
| const valueLabel = document.createElement('span'); | |
| valueLabel.className = 'value-label'; | |
| widthContainer.append(label, slider, valueLabel); | |
| const tocTitle = document.createElement('div'); | |
| tocTitle.className = 'toc-header'; | |
| tocTitle.textContent = 'User Messages'; | |
| const tocList = document.createElement('div'); | |
| tocList.className = 'toc-list'; | |
| container.append(titleBar, widthContainer, tocTitle, tocList); | |
| document.body.appendChild(container); | |
| const initialWidth = localStorage.getItem(STORAGE_KEY_WIDTH) || DEFAULT_WIDTH; | |
| slider.value = String(initialWidth); | |
| updateWidth(initialWidth); | |
| updateSliderStyle(slider); | |
| makeDraggable(titleBar); | |
| slider.addEventListener('input', event => { | |
| updateWidth(event.target.value); | |
| updateSliderStyle(event.target); | |
| }); | |
| } | |
| function hookNavigation() { | |
| const originalPushState = history.pushState; | |
| history.pushState = function () { | |
| originalPushState.apply(this, arguments); | |
| handleNavigation(); | |
| }; | |
| const originalReplaceState = history.replaceState; | |
| history.replaceState = function () { | |
| originalReplaceState.apply(this, arguments); | |
| handleNavigation(); | |
| }; | |
| window.addEventListener('popstate', handleNavigation); | |
| } | |
| function startObserver() { | |
| const observer = new MutationObserver(() => { | |
| clearTimeout(tocUpdateTimeout); | |
| tocUpdateTimeout = setTimeout(() => { | |
| if (location.href !== lastUrl) { | |
| handleNavigation(); | |
| return; | |
| } | |
| const currentMessageCount = document.querySelectorAll(MSG_SELECTOR).length; | |
| if (currentMessageCount !== lastMessageCount) { | |
| updateToc(); | |
| } | |
| }, 500); | |
| }); | |
| observer.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true, | |
| attributes: false | |
| }); | |
| window.m365BookmarkletObserver = observer; | |
| } | |
| createUi(); | |
| hookNavigation(); | |
| updateToc(); | |
| startObserver(); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment