Build a Privacy-First Tampermonkey Script for Long ChatGPT Conversations A developer has built a privacy-first Tampermonkey userscript called ChatGPT Long Conversation Helper that adds collapse and expand controls for individual messages in long ChatGPT conversations. The tool, available on GitHub, operates entirely in the local browser view without uploading, transmitting, or collecting any conversation data, and stores only UI state in localStorage. The script provides per-message collapse buttons, global collapse/expand controls, and a three-line preview to help users navigate lengthy technical planning, code review, and research conversations. Long AI conversations are useful, but they become hard to scan. If you use ChatGPT for technical planning, code review, writing drafts, debugging, or research, a single conversation can easily grow into dozens of turns. At that point, the problem is no longer generating more content. The problem is navigation. You may want to jump back to an earlier question. You may want to hide a long assistant answer after you have already used it. You may want to keep only the most important parts visible while reviewing the whole thread. I wanted a small tool for that specific problem: collapse and expand long ChatGPT questions and answers in the local browser view. The result is ChatGPT Long Conversation Helper , a Tampermonkey userscript that adds per-message collapse controls, global collapse / expand controls, a three-line preview, and local UI state. Companion repository: https://github.com/OnerGit/ChatGPT-Long-Conversation-Helper This is a third-party local userscript. It is not an official OpenAI or ChatGPT feature. It only changes the local browser view. It does not upload, transmit, collect, export, or send conversation content. It does not call the ChatGPT API. It does not automate sending messages. It stores only local UI state in localStorage . A long conversation is useful while you are building it. It becomes less useful when you need to review it later. The page can contain long prompts, detailed answers, code blocks, checklists, and repeated planning notes. Scrolling through everything makes it harder to compare earlier decisions with later results. The tool does not try to summarize the conversation. It keeps the content exactly where it is and adds a local way to hide or show each message. The first version focuses on one narrow workflow improvement: make long conversations easier to review. The userscript adds: Collapse question / Expand question button for user messages; Collapse answer / Expand answer button for assistant messages; Collapse all and Expand all buttons; LCH launcher after hiding the full panel; localStorage .It deliberately does not provide export, scraping, summarization, automation, cloud sync, or API integration. That scope matters. A browser UI helper should not silently become a data extraction tool. This project could eventually become a browser extension, but I did not start there. A Tampermonkey userscript was a better MVP boundary for three reasons. First, it is quick to test. I can paste a single .user.js file into Tampermonkey, open ChatGPT, and validate the DOM behavior immediately. Second, it avoids extension packaging too early. A Chrome or Edge extension would require more decisions around permissions, manifest configuration, distribution, review, and long-term maintenance. Third, the real uncertainty was not packaging. The real uncertainty was whether the DOM-based interaction would feel useful and stable enough. So the first goal was simple: validate the interaction model locally before turning it into a heavier browser extension. Before writing the DOM code, I defined what the tool must not do. The script should not: The only persisted data should be local UI state: whether a message is collapsed and whether the global panel is hidden. That boundary influenced the implementation. The script uses browser APIs such as: querySelectorAll MutationObserver localStorage classList addEventListener It does not need fetch , XMLHttpRequest , WebSocket , sendBeacon , document.cookie , or external dependencies. A userscript starts with metadata. This block tells Tampermonkey where the script should run and which special permissions it needs. For this project, the metadata is intentionally small: // ==UserScript== // @name ChatGPT Long Conversation Helper // @namespace chatgpt-long-conversation-helper // @version 0.1.1 // @description A privacy-first local UI helper that collapses and expands long ChatGPT questions and answers. // @author OnerGit // @match https://chatgpt.com/ // @grant none // @run-at document-idle // @license MIT // ==/UserScript== The important lines are: // @match https://chatgpt.com/ // @grant none // @run-at document-idle @match limits the script to ChatGPT pages. @grant none keeps the script in a simple mode without requesting special Tampermonkey APIs. @run-at document-idle waits until the page is mostly loaded before running. This is useful for UI scripts because many target elements may not exist at the earliest loading stage. This does not guarantee all conversation messages are already present. ChatGPT is a dynamic web app, so the script still needs a MutationObserver . The script needs to find user questions and assistant answers. A tempting approach would be to copy a long selector chain from DevTools. For example, you might inspect a message and copy a selector that includes many nested class names. That is usually fragile. Modern web apps often change generated class names, wrapper elements, or layout structure. A selector that is too deep may break after a small UI update. Instead, this script prefers shallow role-based selectors: js const CONFIG = { roleSelectors: ' data-message-author-role="user" ', ' data-message-author-role="assistant" ' , ignoredAncestors: 'form, textarea, input, nav, aside, header, footer, role="dialog" ', processedAttr: 'data-clch-processed' }; This is still a DOM dependency, and it can break if ChatGPT changes its page structure. But it is more maintainable than relying on a long chain of layout classes. The script also avoids processing input boxes, dialogs, headers, footers, sidebars, and other non-conversation areas. A simplified message finder looks like this: js function getConversationMessageNodes { const found = new Set ; CONFIG.roleSelectors.forEach selector = { document.querySelectorAll selector .forEach node = { if isLikelyConversationMessage node { found.add node ; } } ; } ; return Array.from found ; } The Set prevents duplicates if selectors overlap. A dynamic page can be scanned many times. If the script adds a toolbar to a message every time it scans, the UI will quickly become broken. The solution is to mark processed nodes. function processMessage messageNode { if messageNode || messageNode.getAttribute CONFIG.processedAttr === 'true' { return; } const role = getRole messageNode ; if role == 'user' && role == 'assistant' { return; } messageNode.classList.add 'clch-message' ; messageNode.setAttribute CONFIG.processedAttr, 'true' ; addMessageToolbar messageNode ; restoreState messageNode ; } This makes scanning idempotent. Running scanMessages multiple times should not keep adding more buttons to the same message. That is important when using MutationObserver , because DOM changes may trigger scans repeatedly. For each message, the script inserts a small toolbar before the message node. The toolbar contains one button: js function addMessageToolbar messageNode { const role = getRole messageNode || 'message'; const toolbar = document.createElement 'div' ; toolbar.className = 'clch-toolbar'; const button = document.createElement 'button' ; button.type = 'button'; button.className = 'clch-toggle-button'; button.textContent = getToggleLabel role, false ; button.setAttribute 'aria-expanded', 'true' ; button.addEventListener 'click', = { const currentlyCollapsed = messageNode.getAttribute 'data-clch-collapsed' === 'true'; setCollapsed messageNode, currentlyCollapsed, true ; } ; toolbar.appendChild button ; messageNode.parentNode.insertBefore toolbar, messageNode ; } The button does not move or rewrite the message content. It only toggles a collapsed class on the existing message node. That design choice matters. Moving or wrapping message nodes can introduce layout risk with Markdown tables, code blocks, and wide answer containers. This version avoids re-parenting ChatGPT message DOM nodes and applies the collapsed state directly to the message node. The collapsed state is mostly CSS. The script applies a class such as: clch-collapsed-message Then CSS limits the visible height: .clch-collapsed-message { max-height: calc 3 1.55em ; overflow: hidden important; position: relative important; } A fade mask makes the preview feel less abrupt: .clch-collapsed-message::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 1.9em; pointer-events: none; background: linear-gradient to bottom, rgba 255, 255, 255, 0 , var --clch-fade-bg, ffffff ; } This is intentionally simple. The script does not try to summarize the message. It does not parse the text. It does not store the content. It only changes how much of the existing message is visible. ChatGPT conversations are dynamic. New user messages and assistant replies appear after the initial page load. A one-time scan is not enough. The script uses MutationObserver to watch for newly inserted content: js function startObserver { const target = document.querySelector 'main' || document.body; const observer = new MutationObserver = { window.clearTimeout observerTimer ; observerTimer = window.setTimeout scheduleScan, CONFIG.observerThrottleMs ; } ; observer.observe target, { childList: true, subtree: true } ; } The observer does not process every mutation immediately. It schedules a scan with a small delay. That delay matters because dynamic apps may produce several DOM changes during a single interaction. A small throttle/debounce keeps the script from doing unnecessary repeated work. The scan function can then process any new message nodes that do not already have the data-clch-processed marker. If you collapse several messages and refresh the page, it is useful for the local view to remember that state. The script uses localStorage for this. A simplified storage key looks like this: clch:v0.1.1:/c/example-conversation:assistant:4:collapsed = 1 The key includes: The value is only a UI flag. It does not store message text. The storage helpers are wrapped in try/catch because browser storage can fail or be disabled: function safeGetStorage key { try { return window.localStorage.getItem key ; } catch error { console.warn ' CLCH Failed to read localStorage.', error ; return null; } } function safeSetStorage key, value { try { window.localStorage.setItem key, value ; } catch error { console.warn ' CLCH Failed to write localStorage.', error ; } } This state recovery is best-effort. Because it is index-based, it may not restore perfectly if the conversation order changes or if the page DOM changes. That limitation is acceptable for an MVP because the script is a local UI helper, not a data management system. Individual controls help when reviewing one message. Global controls help when a conversation is already long. The floating panel provides: Collapse all Expand all Hide controls If the panel itself becomes visual noise, it can be hidden into a small LCH launcher. This is a small UI detail, but it matters for a browser helper. A tool that reduces visual noise should not create too much noise of its own. For a small userscript, manual testing is still important. The test plan I used focuses on behavior rather than unit tests: https://chatgpt.com/ . Collapse question . Collapse answer . Collapse all and Expand all . LCH .The privacy test is part of the functional test. For this project, “it works” is not enough. It also needs to stay within the local-only boundary. This is a best-effort UI enhancement. The main limitation is DOM dependency. The script depends on the visible ChatGPT web page structure. If ChatGPT changes its DOM, selectors may need to be updated. Other limitations: These limitations are not hidden because they are part of the engineering reality of a DOM-based userscript. I would keep the next version small. Useful improvements include: A Chrome or Edge extension could also be considered later, but only after the userscript behavior stabilizes. Moving from userscript to extension would require a new review of permissions, storage behavior, privacy documentation, packaging, and distribution. It should not be treated as a simple file conversion. Small local tools can improve AI workflows, but the boundary matters. For this project, the useful feature is not automation. It is navigation. The script does not send messages, call APIs, scrape conversations, or export data. It only changes the local browser view so long conversations are easier to scan. That made Tampermonkey a good starting point. It allowed the core interaction to be tested quickly while keeping the project small enough to review. The broader lesson is simple: when building AI workflow tools, productivity should not come at the cost of unclear data behavior. A small browser tool can still be useful if it has a narrow scope, a clear privacy boundary, and honest limitations. GitHub repository: https://github.com/OnerGit/ChatGPT-Long-Conversation-Helper This is a third-party local userscript, not an official OpenAI or ChatGPT feature.