Adds a "Copy" button to chat message elements to easily copy their content.
As of
// ==UserScript==
// @name Add Copy Button to Chat Messages on Gtihub Copilot web page
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Adds a "Copy" button to chat message elements to easily copy their content.
// @author aspen138
// @match *://github.com/copilot/c/*
// @match *://github.com/copilot/
// @match *://github.com/copilot/*
// @grant none
// @run-at document-end
// @icon https://github.com/favicons/favicon-copilot.svg
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// Update these if class names change
const MESSAGE_CONTENT_CLASS = 'UserMessage-module__container--cAvvK';
const CHAT_MESSAGE_CONTENT_CLASS = 'ChatMessage-module__content--MYneF';
/**
* Creates and returns a copy button element.
*/
function createCopyButton() {
const button = document.createElement('button');
button.innerText = 'Copy';
button.classList.add('copy-button');
// Use sticky positioning to keep it visible while the element is in view
button.style.position = 'sticky';
button.style.top = '10px';
button.style.right = '10px';
button.style.backgroundColor = '#4CAF50';
button.style.color = '#fff';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.padding = '5px 10px';
button.style.cursor = 'pointer';
button.style.fontSize = '0.9em';
button.style.zIndex = '1000';
button.style.boxShadow = '0 2px 6px rgba(0,0,0,0.2)';
button.style.marginLeft = 'auto';
button.style.float = 'right';
button.style.display = 'inline-block';
// Ensure parent or relevant ancestor allows sticky to function
// For sticky to work, the ancestor should have no overflow constraints that break it.
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = '#45a049';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = '#4CAF50';
});
return button;
}
/**
* Adds a copy button to a chat message element.
* @param {HTMLElement} messageElement
*/
function addCopyButton(messageElement) {
// Prevent adding multiple buttons
if (messageElement.querySelector('.copy-button')) return;
const messageContent = messageElement.querySelector(`.${MESSAGE_CONTENT_CLASS}`);
if (!messageContent) return;
// Ensure the parent is a block-level container that supports sticky
messageElement.style.position = 'relative';
messageElement.style.display = 'block';
const copyButton = createCopyButton();
copyButton.addEventListener('click', () => {
const textToCopy = messageContent.innerText.trim();
navigator.clipboard.writeText(textToCopy).then(() => {
copyButton.innerText = 'Copied!';
copyButton.style.backgroundColor = '#388E3C';
setTimeout(() => {
copyButton.innerText = 'Copy';
copyButton.style.backgroundColor = '#4CAF50';
}, 2000);
}).catch(err => {
console.error('Failed to copy text: ', err);
});
});
messageElement.appendChild(copyButton);
}
/**
* Processes all existing chat messages on page load.
*/
function processExistingMessages() {
const messageElements = document.querySelectorAll(`.${CHAT_MESSAGE_CONTENT_CLASS}`);
messageElements.forEach(messageElement => addCopyButton(messageElement));
}
/**
* Observes newly added messages dynamically.
*/
function observeNewMessages() {
const targetNode = document.body;
const config = { childList: true, subtree: true };
const callback = (mutationsList) => {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.classList && node.classList.contains(CHAT_MESSAGE_CONTENT_CLASS)) {
addCopyButton(node);
}
const nestedMessages = node.querySelectorAll(`.${CHAT_MESSAGE_CONTENT_CLASS}`);
nestedMessages.forEach(nestedNode => addCopyButton(nestedNode));
}
});
}
}
};
const observer = new MutationObserver(callback);
observer.observe(targetNode, config);
}
function init() {
processExistingMessages();
observeNewMessages();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();