GitHub - rushil-b-patel/chatGPT-prompt-indexer: Prompt indexing for chatGPT session.

4 min read Original article ↗
chat-gpt-ext.webm

A Chrome extension that injects a floating, draggable sidebar into ChatGPT. It auto-indexes all your prompts in a conversation and lets you click any one to instantly scroll back to it.


Demo

Open a long ChatGPT conversation, the sidebar appears automatically on the right side of the screen.


Features

  • Indexes every user prompt in the current conversation
  • Click any prompt to smooth-scroll to it
  • Search/filter prompts
  • Draggable widget, reposition anywhere on screen
  • Adapts to ChatGPT's theme

Quick Start

# 1. Clone the repo
git clone https://github.com/rushil-b-patel/chatgpt-index-ext

# 2. Enter the project folder
cd chatgpt-index-ext

# 3. Install dependencies
npm install

# 4. Build the extension
npm run build
  • open chrome://extensions/
  • enable Developer Mode
  • click Load Unpacked
  • select the dist/ folder of this cloned repo.

Project Structure

src/
  content/
    modules/
      Extractor.js     ← Scrapes user prompts from ChatGPT's DOM
      Observer.js      ← Watches DOM for changes
    views/
      App.jsx          ← Floating sidebar UI (React)
      App.css          ← Tailwind styles
    main.jsx           ← (Scripts start here) mounts React app, wires everything together
  popup/
    App.jsx            ← Toolbar popup UI (UI when you click on the extension icon)
    index.html
    main.jsx
manifest.config.js     ← Chrome extension manifest file (MV3)
vite.config.js         ← Build config

Architecture Overview

The extension has two independent parts:

Part Location Purpose
Content Script src/content/ Injected into chatgpt.com. Scrapes DOM, runs observer, mounts UI.
Popup src/popup/ Shown when you click the extension icon. Static info only.

The content script is further split into three responsibilities:

main.jsx  ──────►  Extractor.js   (reads DOM → returns prompts array)
    │
    └───────────►  Observer.js    (watches DOM → triggers re-extraction)
    │
    └───────────►  App.jsx        (React UI → receives prompts via postMessage)

How It Works: Full Flow

Step 1: Chrome injects the content script

The manifest declares:

content_scripts: [{
  js: ['src/content/main.jsx'],
  matches: ['https://chatgpt.com/*'],
}]

Whenever you open any page on chatgpt.com, Chrome automatically runs main.jsx inside that page's context.


Step 2: React app is mounted into ChatGPT's DOM

main.jsx creates a new <div> and appends it to ChatGPT's document.body, then mounts the React app inside it:

const container = document.createElement('div');
container.id = 'crxjs-app';
document.body.appendChild(container);
createRoot(container).render(<App />);

The floating sidebar now exists in the page but renders nothing until prompts are found.


Step 3: Prompts are extracted from the DOM

Extractor.js queries ChatGPT's DOM for user messages using the attribute ChatGPT applies to every user turn:

document.querySelectorAll('[data-message-author-role="user"]')

For each element found, it pulls the inner text from the .whitespace-pre-wrap child node (where the actual message text lives), trims it, and filters out blanks.

Result: a clean array like:

["What is React?", "How does useState work?", "Explain useEffect"]

Step 4: Prompts are sent to the React app via postMessage

main.jsx calls updatePrompts(), which runs the extractor and posts the result to the window:

function updatePrompts() {
  const prompts = Extractor.extractPrompts();
  if (prompts.length > 0 || window.location.href.includes('chatgpt.com')) {
    window.postMessage({ type: 'PROMPT_INDEX_UPDATE', prompts }, '*');
  }
}

The React app listens for this message:

window.addEventListener('message', (event) => {
  if (event.source !== window || event.data?.type !== 'PROMPT_INDEX_UPDATE') return;
  setPrompts(event.data.prompts || []);
});

Step 5: DOM changes are watched via MutationObserver

Observer.js wraps the browser's native MutationObserver. It watches document.body for any DOM mutations (new nodes, text changes, subtree changes):

this.observer.observe(document.body, {
  childList: true,
  subtree: true,
  characterData: true,
});

Every mutation triggers updatePrompts() — but with a 500ms debounce. This is critical because ChatGPT streams responses token-by-token, causing dozens of DOM mutations per second. Without debouncing, updatePrompts() would fire hundreds of times unnecessarily.


Step 6: SPA navigation is handled

ChatGPT is a Single Page App — switching conversations doesn't reload the page. The browser's History API is used to change the URL silently.

main.jsx listens for navigation events to re-index on conversation switch:

window.addEventListener('popstate', () => setTimeout(updatePrompts, 500));
window.addEventListener('pushstate', () => setTimeout(updatePrompts, 500));
window.addEventListener('replacestate', () => setTimeout(updatePrompts, 500));
Event Fired by When
popstate Browser natively Back / Forward button
pushstate Must be dispatched manually history.pushState() call
replacestate Must be dispatched manually history.replaceState() call

The setTimeout(..., 500) gives ChatGPT 500ms to finish rendering the new conversation before scraping.


Step 7: The UI renders the index

App.jsx renders the floating widget. Key behaviors: