{"slug": "build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations", "title": "Build a Privacy-First Tampermonkey Script for Long ChatGPT Conversations", "summary": "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.", "body_md": "Long AI conversations are useful, but they become hard to scan.\n\nIf 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.\n\nYou 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.\n\nI wanted a small tool for that specific problem: collapse and expand long ChatGPT questions and answers in the local browser view.\n\nThe 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.\n\nCompanion repository:\n\n```\nhttps://github.com/OnerGit/ChatGPT-Long-Conversation-Helper\n```\n\nThis is a third-party local userscript. It is not an official OpenAI or ChatGPT feature.\n\nIt 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`\n\n.\n\nA long conversation is useful while you are building it. It becomes less useful when you need to review it later.\n\nThe 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.\n\nThe 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.\n\nThe first version focuses on one narrow workflow improvement: make long conversations easier to review.\n\nThe userscript adds:\n\n`Collapse question`\n\n/ `Expand question`\n\nbutton for user messages;`Collapse answer`\n\n/ `Expand answer`\n\nbutton for assistant messages;`Collapse all`\n\nand `Expand all`\n\nbuttons;`LCH`\n\nlauncher after hiding the full panel;`localStorage`\n\n.It deliberately does not provide export, scraping, summarization, automation, cloud sync, or API integration.\n\nThat scope matters. A browser UI helper should not silently become a data extraction tool.\n\nThis project could eventually become a browser extension, but I did not start there.\n\nA Tampermonkey userscript was a better MVP boundary for three reasons.\n\nFirst, it is quick to test. I can paste a single `.user.js`\n\nfile into Tampermonkey, open ChatGPT, and validate the DOM behavior immediately.\n\nSecond, 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.\n\nThird, the real uncertainty was not packaging. The real uncertainty was whether the DOM-based interaction would feel useful and stable enough.\n\nSo the first goal was simple: validate the interaction model locally before turning it into a heavier browser extension.\n\nBefore writing the DOM code, I defined what the tool must not do.\n\nThe script should not:\n\nThe only persisted data should be local UI state: whether a message is collapsed and whether the global panel is hidden.\n\nThat boundary influenced the implementation. The script uses browser APIs such as:\n\n```\nquerySelectorAll\nMutationObserver\nlocalStorage\nclassList\naddEventListener\n```\n\nIt does not need `fetch`\n\n, `XMLHttpRequest`\n\n, `WebSocket`\n\n, `sendBeacon`\n\n, `document.cookie`\n\n, or external dependencies.\n\nA userscript starts with metadata. This block tells Tampermonkey where the script should run and which special permissions it needs.\n\nFor this project, the metadata is intentionally small:\n\n```\n// ==UserScript==\n// @name         ChatGPT Long Conversation Helper\n// @namespace    chatgpt-long-conversation-helper\n// @version      0.1.1\n// @description  A privacy-first local UI helper that collapses and expands long ChatGPT questions and answers.\n// @author       OnerGit\n// @match        https://chatgpt.com/*\n// @grant        none\n// @run-at       document-idle\n// @license      MIT\n// ==/UserScript==\n```\n\nThe important lines are:\n\n```\n// @match        https://chatgpt.com/*\n// @grant        none\n// @run-at       document-idle\n```\n\n`@match`\n\nlimits the script to ChatGPT pages.\n\n`@grant none`\n\nkeeps the script in a simple mode without requesting special Tampermonkey APIs.\n\n`@run-at document-idle`\n\nwaits 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.\n\nThis does not guarantee all conversation messages are already present. ChatGPT is a dynamic web app, so the script still needs a `MutationObserver`\n\n.\n\nThe script needs to find user questions and assistant answers.\n\nA 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.\n\nThat is usually fragile.\n\nModern 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.\n\nInstead, this script prefers shallow role-based selectors:\n\n``` js\nconst CONFIG = {\n  roleSelectors: [\n    '[data-message-author-role=\"user\"]',\n    '[data-message-author-role=\"assistant\"]'\n  ],\n  ignoredAncestors: 'form, textarea, input, nav, aside, header, footer, [role=\"dialog\"]',\n  processedAttr: 'data-clch-processed'\n};\n```\n\nThis 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.\n\nThe script also avoids processing input boxes, dialogs, headers, footers, sidebars, and other non-conversation areas.\n\nA simplified message finder looks like this:\n\n``` js\nfunction getConversationMessageNodes() {\n  const found = new Set();\n\n  CONFIG.roleSelectors.forEach((selector) => {\n    document.querySelectorAll(selector).forEach((node) => {\n      if (isLikelyConversationMessage(node)) {\n        found.add(node);\n      }\n    });\n  });\n\n  return Array.from(found);\n}\n```\n\nThe `Set`\n\nprevents duplicates if selectors overlap.\n\nA dynamic page can be scanned many times.\n\nIf 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.\n\n```\nfunction processMessage(messageNode) {\n  if (!messageNode || messageNode.getAttribute(CONFIG.processedAttr) === 'true') {\n    return;\n  }\n\n  const role = getRole(messageNode);\n\n  if (role !== 'user' && role !== 'assistant') {\n    return;\n  }\n\n  messageNode.classList.add('clch-message');\n  messageNode.setAttribute(CONFIG.processedAttr, 'true');\n\n  addMessageToolbar(messageNode);\n  restoreState(messageNode);\n}\n```\n\nThis makes scanning idempotent. Running `scanMessages()`\n\nmultiple times should not keep adding more buttons to the same message.\n\nThat is important when using `MutationObserver`\n\n, because DOM changes may trigger scans repeatedly.\n\nFor each message, the script inserts a small toolbar before the message node.\n\nThe toolbar contains one button:\n\n``` js\nfunction addMessageToolbar(messageNode) {\n  const role = getRole(messageNode) || 'message';\n\n  const toolbar = document.createElement('div');\n  toolbar.className = 'clch-toolbar';\n\n  const button = document.createElement('button');\n  button.type = 'button';\n  button.className = 'clch-toggle-button';\n  button.textContent = getToggleLabel(role, false);\n  button.setAttribute('aria-expanded', 'true');\n\n  button.addEventListener('click', () => {\n    const currentlyCollapsed =\n      messageNode.getAttribute('data-clch-collapsed') === 'true';\n\n    setCollapsed(messageNode, !currentlyCollapsed, true);\n  });\n\n  toolbar.appendChild(button);\n  messageNode.parentNode.insertBefore(toolbar, messageNode);\n}\n```\n\nThe button does not move or rewrite the message content. It only toggles a collapsed class on the existing message node.\n\nThat 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.\n\nThe collapsed state is mostly CSS.\n\nThe script applies a class such as:\n\n```\nclch-collapsed-message\n```\n\nThen CSS limits the visible height:\n\n```\n.clch-collapsed-message {\n  max-height: calc(3 * 1.55em);\n  overflow: hidden !important;\n  position: relative !important;\n}\n```\n\nA fade mask makes the preview feel less abrupt:\n\n```\n.clch-collapsed-message::after {\n  content: \"\";\n  position: absolute;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  height: 1.9em;\n  pointer-events: none;\n  background: linear-gradient(\n    to bottom,\n    rgba(255, 255, 255, 0),\n    var(--clch-fade-bg, #ffffff)\n  );\n}\n```\n\nThis 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.\n\nChatGPT conversations are dynamic. New user messages and assistant replies appear after the initial page load.\n\nA one-time scan is not enough.\n\nThe script uses `MutationObserver`\n\nto watch for newly inserted content:\n\n``` js\nfunction startObserver() {\n  const target = document.querySelector('main') || document.body;\n\n  const observer = new MutationObserver(() => {\n    window.clearTimeout(observerTimer);\n    observerTimer = window.setTimeout(scheduleScan, CONFIG.observerThrottleMs);\n  });\n\n  observer.observe(target, {\n    childList: true,\n    subtree: true\n  });\n}\n```\n\nThe observer does not process every mutation immediately. It schedules a scan with a small delay.\n\nThat 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.\n\nThe scan function can then process any new message nodes that do not already have the `data-clch-processed`\n\nmarker.\n\nIf you collapse several messages and refresh the page, it is useful for the local view to remember that state.\n\nThe script uses `localStorage`\n\nfor this.\n\nA simplified storage key looks like this:\n\n```\nclch:v0.1.1:/c/example-conversation:assistant:4:collapsed = 1\n```\n\nThe key includes:\n\nThe value is only a UI flag.\n\nIt does not store message text.\n\nThe storage helpers are wrapped in `try/catch`\n\nbecause browser storage can fail or be disabled:\n\n```\nfunction safeGetStorage(key) {\n  try {\n    return window.localStorage.getItem(key);\n  } catch (error) {\n    console.warn('[CLCH] Failed to read localStorage.', error);\n    return null;\n  }\n}\n\nfunction safeSetStorage(key, value) {\n  try {\n    window.localStorage.setItem(key, value);\n  } catch (error) {\n    console.warn('[CLCH] Failed to write localStorage.', error);\n  }\n}\n```\n\nThis 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.\n\nThat limitation is acceptable for an MVP because the script is a local UI helper, not a data management system.\n\nIndividual controls help when reviewing one message. Global controls help when a conversation is already long.\n\nThe floating panel provides:\n\n`Collapse all`\n\n`Expand all`\n\n`Hide controls`\n\nIf the panel itself becomes visual noise, it can be hidden into a small `LCH`\n\nlauncher.\n\nThis 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.\n\nFor a small userscript, manual testing is still important.\n\nThe test plan I used focuses on behavior rather than unit tests:\n\n`https://chatgpt.com/`\n\n.`Collapse question`\n\n.`Collapse answer`\n\n.`Collapse all`\n\nand `Expand all`\n\n.`LCH`\n\n.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.\n\nThis is a best-effort UI enhancement.\n\nThe 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.\n\nOther limitations:\n\nThese limitations are not hidden because they are part of the engineering reality of a DOM-based userscript.\n\nI would keep the next version small.\n\nUseful improvements include:\n\nA Chrome or Edge extension could also be considered later, but only after the userscript behavior stabilizes.\n\nMoving 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.\n\nSmall local tools can improve AI workflows, but the boundary matters.\n\nFor 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.\n\nThat made Tampermonkey a good starting point. It allowed the core interaction to be tested quickly while keeping the project small enough to review.\n\nThe 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.\n\nGitHub repository:\n\n```\nhttps://github.com/OnerGit/ChatGPT-Long-Conversation-Helper\n```\n\nThis is a third-party local userscript, not an official OpenAI or ChatGPT feature.", "url": "https://wpnews.pro/news/build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations", "canonical_source": "https://dev.to/bob_oner/build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations-2765", "published_at": "2026-05-28 12:54:50+00:00", "updated_at": "2026-05-28 13:22:59.498162+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "generative-ai", "natural-language-processing", "ai-products"], "entities": ["ChatGPT", "OpenAI", "Tampermonkey", "GitHub", "OnerGit"], "alternates": {"html": "https://wpnews.pro/news/build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations", "markdown": "https://wpnews.pro/news/build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations.md", "text": "https://wpnews.pro/news/build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations.txt", "jsonld": "https://wpnews.pro/news/build-a-privacy-first-tampermonkey-script-for-long-chatgpt-conversations.jsonld"}}