Skip to content

Instantly share code, notes, and snippets.

@kiranwayne
Last active June 29, 2026 01:06
Show Gist options
  • Select an option

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

Select an option

Save kiranwayne/0eb8395422892f5768c973e8c026c9aa to your computer and use it in GitHub Desktop.
// ==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