A technical deep dive into how browser extensions extract conversation data from AI platforms like ChatGPT, Claude, and Gemini using DOM parsing, Shadow DOM traversal, content scripts, and Manifest V3 architecture.
Founder & Lead Engineer at Toffee
To transfer AI context between platforms, you first need to extract it. The conversation data displayed in the browser — the user prompts, model responses, code blocks, and metadata — must be captured in a structured format before any compression or transfer can occur.
There are theoretically multiple approaches to capturing this data: official APIs, browser automation, or direct DOM extraction via browser extensions. In practice, browser extensions are the only viable approach for several reasons:
This article explores the technical architecture of how browser extensions — specifically Chromium-based Manifest V3 extensions — capture AI conversation data from the DOM.
A browser extension's content script is JavaScript that runs in the context of a web page. It has full access to the page's DOM but operates in an isolated execution environment (the "isolated world"), meaning it cannot access the page's JavaScript variables or functions directly.
For AI context capture, content scripts are injected into specific domains. In the extension's manifest, you define which sites the content scripts should run on:
{
"content_scripts": [
{
"matches": ["https://chatgpt.com/*"],
"js": ["content-scripts/chatgpt.js"],
"run_at": "document_idle"
},
{
"matches": ["https://claude.ai/*"],
"js": ["content-scripts/claude.js"],
"run_at": "document_idle"
},
{
"matches": ["https://gemini.google.com/*"],
"js": ["content-scripts/gemini.js"],
"run_at": "document_idle"
}
]
}
Each AI platform has its own content script because the DOM structure, CSS selectors, and data attributes are completely different. This per-platform adapter pattern is essential for maintainability — when ChatGPT updates its UI (which happens frequently), only the ChatGPT adapter needs to be updated.
Each content script implements a common interface:
interface PlatformAdapter {
platform: string;
detectPlatform(): boolean;
extractConversation(): ConversationData;
getMessageElements(): HTMLElement[];
parseMessage(element: HTMLElement): ParsedMessage;
injectContext(bundle: ToffeeBundle): void;
}
This interface ensures that the core extension logic — the popup UI, the background service worker, and the compression engine — can work with any platform without knowing the implementation details of DOM parsing.
Each AI platform structures its conversation UI differently. Here is how extraction works for the three major platforms:
ChatGPT uses React-rendered HTML with specific data attributes that identify message roles. The key selector is the data-message-author-role attribute, which distinguishes between user and assistant messages:
function extractChatGPT(): ConversationData {
const messages: ParsedMessage[] = [];
// ChatGPT renders each message in a div with a role attribute
const messageElements = document.querySelectorAll(
'[data-message-author-role]'
);
messageElements.forEach((el) => {
const role = el.getAttribute('data-message-author-role');
const contentEl = el.querySelector('.markdown.prose');
if (contentEl && role) {
messages.push({
role: role === 'user' ? 'user' : 'assistant',
content: contentEl.innerHTML,
textContent: contentEl.textContent || '',
});
}
});
return { platform: 'chatgpt', messages };
}
ChatGPT's DOM is relatively straightforward because messages are rendered in the main document with accessible data attributes. However, ChatGPT frequently updates its UI, so selectors must be validated with each extension update.
Claude uses a more nested component structure. Messages are wrapped in conversation-turn containers, and the role distinction is made through CSS classes rather than data attributes:
function extractClaude(): ConversationData {
const messages: ParsedMessage[] = [];
// Claude uses font-specific classes for role identification
const turns = document.querySelectorAll(
'[class*="font-claude"], [class*="font-user"]'
);
turns.forEach((el) => {
const isAssistant = el.className.includes('font-claude');
const contentContainer = el.querySelector('.grid-cols-1');
if (contentContainer) {
messages.push({
role: isAssistant ? 'assistant' : 'user',
content: contentContainer.innerHTML,
textContent: contentContainer.textContent || '',
});
}
});
return { platform: 'claude', messages };
}
Microsoft Copilot is the most technically challenging platform to extract from because it uses Web Components with Shadow DOM encapsulation. Standard querySelector calls cannot reach into Shadow DOM boundaries.
Extracting from Copilot requires a recursive Shadow DOM traversal function:
function deepQuerySelectorAll(
root: Document | ShadowRoot | Element,
selector: string
): Element[] {
const results: Element[] = [];
// Search the current level
results.push(...Array.from(root.querySelectorAll(selector)));
// Recursively search shadow roots
const allElements = root.querySelectorAll('*');
allElements.forEach((el) => {
if (el.shadowRoot) {
results.push(
...deepQuerySelectorAll(el.shadowRoot, selector)
);
}
});
return results;
}
This function traverses the entire DOM tree, including all shadow boundaries, to find message elements regardless of their nesting depth. This is computationally more expensive than standard DOM queries, so it should be called judiciously — ideally only when the user triggers a capture action.
Content scripts cannot directly access browser extension APIs like storage or network requests in Manifest V3. Instead, they communicate with the background service worker through Chrome's message passing system:
// Content script: Send captured data to background
chrome.runtime.sendMessage({
type: 'CAPTURE_RESULT',
payload: {
platform: 'chatgpt',
messages: extractedMessages,
capturedAt: new Date().toISOString(),
url: window.location.href,
}
});
// Background service worker: Receive and process
chrome.runtime.onMessage.addListener(
(message, sender, sendResponse) => {
if (message.type === 'CAPTURE_RESULT') {
processCapture(message.payload)
.then((bundle) => {
// Store in IndexedDB via Dexie
db.bundles.put(bundle);
sendResponse({ success: true, bundleId: bundle.id });
})
.catch((error) => {
sendResponse({ success: false, error: error.message });
});
return true; // Keep channel open for async response
}
}
);
The return true statement is critical — without it, the message channel closes before the async processing completes, and the content script never receives the response. This is a common source of bugs in extension development.
The background service worker then orchestrates the compression pipeline, stores the resulting bundle in IndexedDB (using Dexie for typed access), and optionally syncs it to the cloud if the user has enabled synchronization.
AI platforms stream their responses token by token. If you capture the DOM while a response is still streaming, you will get an incomplete message. Content scripts must detect when a response is complete before triggering extraction.
The most reliable approach is to observe DOM mutations and detect when new tokens stop being added:
function waitForStreamComplete(
messageElement: HTMLElement
): Promise {
return new Promise((resolve) => {
let timeout: ReturnType;
const observer = new MutationObserver(() => {
// Reset timer on each DOM change
clearTimeout(timeout);
timeout = setTimeout(() => {
// No changes for 1.5s — streaming is complete
observer.disconnect();
resolve();
}, 1500);
});
observer.observe(messageElement, {
childList: true,
subtree: true,
characterData: true,
});
// Fallback: resolve after 30s regardless
setTimeout(() => {
observer.disconnect();
resolve();
}, 30000);
});
}
This MutationObserver watches for DOM changes within the message element. When changes stop for 1.5 seconds, the response is considered complete. A 30-second fallback prevents indefinite waiting if the observer misses the end of streaming.
This pattern — debounced mutation observation — is widely used in browser extension development for detecting page state changes. It is more reliable than checking for specific "stop generating" button states, which vary across platforms and UI updates.
Browser extensions that access page content operate in a position of significant trust. The content script has read access to everything visible on the page. This creates several security responsibilities:
Extensions should request only the minimum permissions needed. For AI context capture, this means:
activeTab — Access only the current tab's content when the user explicitly clicks the extension<all_urls>)Captured conversation data must be stored securely. Using the browser's IndexedDB with Dexie provides structured storage that is isolated per extension origin. Data never leaves the browser unless the user explicitly triggers cloud sync.
Manifest V3 enforces strict Content Security Policies that prevent inline script execution and restrict network requests. Content scripts must be bundled as static files — they cannot be dynamically loaded. This prevents a class of injection attacks that were possible in Manifest V2.
For AI context capture specifically, the extension never executes code from the AI platform's page, never modifies the page's functionality, and never intercepts network requests. It only reads the DOM structure that is already rendered and visible to the user.
No. The content script reads only the visible DOM content — the same text you see on screen. It does not access cookies, API tokens, or authentication credentials for the AI platform.
Each AI platform has a dedicated adapter (content script) with platform-specific selectors. When a platform updates its DOM structure, only that adapter needs to be updated. The extension is tested against all supported platforms before each release.
No. The extension only runs on specific, pre-declared AI platform domains (chatgpt.com, claude.ai, gemini.google.com, etc.). It does not and cannot access any other websites.
A comprehensive technical guide to Chrome Manifest V3 architecture — service workers, content scripts, message passing, storage, and the real-world challenges of building production extensions for AI platforms.
Learn how AI context transfer works, why language models lose memory between sessions, and how protocols like Toffee compress and port conversation context across ChatGPT, Claude, and Gemini without data loss.
A deep technical exploration of semantic compression — the process of reducing AI conversation transcripts by 60-80% while preserving entities, decisions, preferences, and structural relationships.