From 54894a7a4d792d8d2f3725dd9bfe5d9a6be684ed Mon Sep 17 00:00:00 2001 From: webbrain-one <295484252+webbrain-one@users.noreply.github.com> Date: Mon, 27 Jul 2026 11:22:33 +0300 Subject: [PATCH] feat: add 'Copy' button to ChatGPT saved memories modal Adds a userscript that inserts a copy button next to the remove icon in the Personalization settings memories modal, allowing users to quickly copy memory text to the clipboard. --- README.md | 1 + userscripts/chatgpt-memories-copy/README.md | 16 +++ .../chatgpt-memories-copy.user.js | 100 ++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 userscripts/chatgpt-memories-copy/README.md create mode 100644 userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js diff --git a/README.md b/README.md index caa6345..64370da 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ My user scripts to add functionality to various sites around the web (that were - [Freshdesk Make 'Redactor Editor' Resizeable](./userscripts/freshdesk-make-redactor-editor-resizeable/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/freshdesk-make-redactor-editor-resizeable/freshdesk-make-redactor-editor-resizeable.user.js)) - [GitHub Gist - CodeMirror Resizer](./userscripts/github-gist-codemirror-resizer/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/github-gist-codemirror-resizer/github-gist-codemirror-resizer.user.js)) - [GitHub Notifications - Arrow key navigation](./userscripts/github-notifications-arrow-key-navigation/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/github-notifications-arrow-key-navigation/github-notifications-arrow-key-navigation.user.js)) +- [ChatGPT Memories Copy Button](./userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js)) - [Google developer sites - Ensure ?authuser= by default](./userscripts/google-developer-sites-ensure-authuser/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/google-developer-sites-ensure-authuser/google-developer-sites-ensure-authuser.user.js)) - [Nourishd Meal Highlighter](./userscripts/nourishd-meal-highlighter/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/nourishd-meal-highlighter/nourishd-meal-highlighter.user.js)) - [YouTube Speed Override](./userscripts/youtube-speed-override/) ([Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/youtube-speed-override/youtube-speed-override.user.js)) diff --git a/userscripts/chatgpt-memories-copy/README.md b/userscripts/chatgpt-memories-copy/README.md new file mode 100644 index 0000000..8eaefa7 --- /dev/null +++ b/userscripts/chatgpt-memories-copy/README.md @@ -0,0 +1,16 @@ +# ChatGPT Memories Copy Button + +```javascript +// ==UserScript== +// @name ChatGPT Memories Copy Button +// @namespace https://www.devalias.net/ +// @version 0.4 +// @description Add a "copy" button next to the trash icon for ChatGPT user memories to copy memory text easily. +// @author Glenn 'devalias' Grant +// @match https://chatgpt.com/c/*#settings/Personalization* +// @match https://chat.openai.com/c/*#settings/Personalization* +// @icon https://cdn.oaistatic.com/assets/favicon-eex17e9e.ico +// ==/UserScript== +``` + +- [Install](https://github.com/0xdevalias/userscripts/raw/main/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js) diff --git a/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js b/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js new file mode 100644 index 0000000..f7bcf0e --- /dev/null +++ b/userscripts/chatgpt-memories-copy/chatgpt-memories-copy.user.js @@ -0,0 +1,100 @@ +// ==UserScript== +// @name ChatGPT Memories Copy Button +// @namespace https://www.devalias.net/ +// @version 0.4 +// @description Add a "copy" button next to the trash icon for ChatGPT user memories to copy memory text easily. +// @author Glenn 'devalias' Grant +// @match https://chatgpt.com/c/*#settings/Personalization* +// @match https://chat.openai.com/c/*#settings/Personalization* +// @icon https://cdn.oaistatic.com/assets/favicon-eex17e9e.ico +// ==/UserScript== + +(function() { + 'use strict'; + + function handleCopyClick(textCell, btn) { + const text = textCell.textContent.trim(); + navigator.clipboard.writeText(text).then(() => { + // Visual feedback by temporarily changing color + btn.classList.add('text-token-text-secondary'); + setTimeout(() => btn.classList.remove('text-token-text-secondary'), 800); + }); + } + + function createCopyButton(textCell) { + const btn = document.createElement('button'); + btn.type = 'button'; + btn.ariaLabel = 'Copy'; + btn.className = 'text-token-text-tertiary hover:text-token-text-secondary'; + btn.innerHTML = ` + + + + + `; + btn.addEventListener('click', () => handleCopyClick(textCell, btn)); // explicit btn arg + return btn; + } + + function injectCopyButtons() { + document.querySelectorAll('[data-testid="modal-memories"] table tbody tr').forEach(row => { + const textCell = row.querySelector('.text-left'); + const actionCell = row.querySelector('.text-right'); + if (!textCell || !actionCell) return; + if (actionCell.querySelector('button[aria-label="Copy"]')) return; // already added + + const copyBtn = createCopyButton(textCell); + + const trashBtn = actionCell.querySelector('button[aria-label="Remove"]'); + if (trashBtn) { + actionCell.insertBefore(copyBtn, trashBtn); + } else { + // fallback: put it first so it precedes any other future actions + actionCell.prepend(copyBtn); + } + }); + } + + const memoriesModalTbody = document.querySelector('[data-testid="modal-memories"] table tbody'); + + if (memoriesModalTbody) { + const observer = new MutationObserver(mutations => { + // MutationRecord structure reference: + // { + // type: "childList" | "attributes" | "characterData", + // target: Node, // node that changed + // addedNodes: NodeList, // newly added children + // removedNodes: NodeList, // removed children + // previousSibling: Node | null, + // nextSibling: Node | null, + // attributeName: string | null, + // attributeNamespace: string | null, + // oldValue: string | null + // } + + for (const m of mutations) { + // NOTE: could also use mutations.find(...) for conciseness, but loop is clearer + if ( + m.type === "childList" && + ([...m.addedNodes, ...m.removedNodes].some(n => n.nodeName === "TR")) + ) { + // Debug logging to help refine filtering: + // console.debug("Relevant mutation:", m); + + injectCopyButtons(); + break; // stop after first relevant + } + } + }); + + observer.observe(memoriesModalTbody, { childList: true }); + // ⚠️ If the modal isn't yet present when this script runs, + // we may need to observe a higher-level container until it appears. + } else { + // Fallback: tbody not found at script init. + // Might need to retry later or observe the modal root itself. + // console.warn("Memories modal tbody not found; observer not started"); + } + + injectCopyButtons(); +})();