A ChatGPT update shipped on a Tuesday. By Wednesday morning, Snapshot was capturing empty threads.
Nothing in my code changed. Their DOM did.
Web AI Chats Are Not Documents
Open ChatGPT. Right-click. View Source.
What you see is not a conversation. It's a React app. Dynamic DOM nodes. Lazy-loaded images. Message containers that change class names between releases.
When you copy-paste from ChatGPT into Claude, you lose structure. Code blocks flatten. Images vanish. Turn boundaries disappear. You're not moving a conversation. You're moving a fragment.
Previously, I explained why I chose Context PDF as the restoration container. But a container is useless without clean input. ZIP import produces that input for existing conversations. Snapshot produces it for live/new ones.
Who Needs This (and Why It's a Pro Feature)
Let me put on the product hat before we go technical.
In a previous article, I described the two user groups I built ChatShuttle for. Developers and one-person companies who subscribe to multiple models, switching by task, hitting context limits constantly. Content creators who use one AI for brainstorming, another for polish, another for structure, and can't afford to lose their persona, tone, or iteration history.
Both groups share a trait: they don't use one AI. They use three or four. Gemini for image generation. ChatGPT for planning and overall structure. Claude for technical analysis and sample code.
They're not migrating away from one AI. They're actively switching between them, constantly, using each model where it's strongest.
These users can already import their AI chat history through exports. ChatShuttle supports incremental import, so they don't have to re-import everything each time. But export-then-import is extra steps. Download the ZIP. Drag it in. Wait for processing.
When you're in the middle of a complex task and need to move a 30-turn thread from ChatGPT to Claude right now, before you hit the context window limit, those extra steps feel like friction.
Snapshot removes those steps. One click. The thread is captured from the live page. No export file needed.
When I identified this use case, it became clear: this is the feature that would motivate power users to upgrade. Not because the engineering is complex (though it is), but because the urgency is real. They need it now, not through a pipeline.
Building Snapshot: Every Step Has a Trap
Every web AI renders conversations differently. ChatGPT, Claude, and Gemini each have their own DOM structure for displaying messages, storing images, and organizing turns. None of them publish a stable API for conversation access. All undocumented. All proprietary.
I thought the hard part would be the restore logic. I was wrong. The capture logic is where most of the pain lives.
Finding the conversation
You'd think this is the easy part. It's not.
The naive approach: hardcode a CSS selector like .message-container. This works until the platform ships a redesign. Then it matches nothing.
I learned this the hard way after a ChatGPT update broke my selectors overnight. The fix was to stop relying on class names and start looking for structural anchors: alternating message containers, role indicators, the tree hierarchy of "a list of messages, each with a role and content." Class names change. That hierarchy tends to stay.
When none of the selectors match, the system fails explicitly. Capturing garbage and pretending it worked would be worse.
Getting the text right
Once I could find the conversation, the next challenge was extracting it without losing meaning.
Every message has a role: user or assistant. That distinction has to survive extraction, or the restored conversation is unreadable. Code blocks need language tags preserved. Markdown formatting can't collapse into plain text.
But each platform renders these differently. Some inline code with special characters that get mangled during extraction. Others wrap markdown in nested containers that introduce phantom whitespace.
The mitigation: normalize aggressively after extraction. Strip rendering artifacts. Validate that code blocks open and close properly. Check that roles alternate correctly.
This normalization step is also why the search index I built previously works at all. If the captured content is noisy, search results are noisy. Snapshot quality directly determines search quality.
Images: the hardest problem
Images are where the real tradeoffs live.
Within a conversation, images fall into two categories: images the user uploaded (screenshots, diagrams) and images the AI generated. Both need to be captured and paired to the correct turn.
The DOM renders images as <img> elements. The source might be a blob URL (temporary, expires when the tab closes), a data URI, or a CDN link (may require authentication). I had to learn to capture image data immediately at snapshot time, not lazily. Wait too long and the blob URL is already dead.
But not all images are <img> tags.
Claude stores certain images inside canvas elements. To properly extract images from canvas, you'd need a multimodal vision model to interpret the content, or specialized rendering tools. But adding those tools would violate the principles I set early in this series: local-first, lightweight browser extension, no external service dependencies. A vision model running inside a Chrome extension is not lightweight. An API call to a cloud vision service is not local-first.
So I made a tradeoff. Image capture from Claude is limited compared to ChatGPT. The user experience gap is real. I manage expectations by marking this as experimental.
Storing it so Restore can use it
After extraction, the raw data needs a stable schema that Restore can consume regardless of which platform it came from.
The output: an ordered list of turns, each with a role, text content, and optional image attachments. This is what gets synced to Google Drive. This is what the Context PDF is generated from (as I described in a previous article).
The trap here is schema drift. If I change the internal format without versioning, older snapshots become unreadable. The mitigation: version the schema from day one. The Repair function, which re-scans Drive and rebuilds the local index, acts as recovery when data falls out of sync.
What I Can't Control
Some limitations aren't engineering decisions. They're hard ceilings.
I can only capture what the browser shows me. If information isn't rendered in the DOM, it doesn't exist for Snapshot.
And when a platform pushes a frontend update, the DOM structure can change without warning. New class names. Restructured message containers. Different image loading strategies. When that happens, Snapshot breaks.
"Restore Button Does Nothing" is a known failure mode. The immediate fix is refreshing the page to re-establish the extension's DOM connection. But when the selectors themselves no longer match, I need to ship a code update.
This is recurring engineering work. Not a one-time build. It's the maintenance loop I had in mind when I made my pricing decision. These features have entropy. They require ongoing attention.
What's Next
Once I identified who my users are, feature decisions started following naturally. Snapshot exists because these users need to move threads between web AIs in real time. But the same users don't just chat with web AIs. They also work with code agents.
Developers using Claude Code, Cursor, or Antigravity are often working on the same problems they were discussing in ChatGPT or Gemini an hour earlier. The topics overlap. The context is related.
That means the conversation memory sitting in Google Drive isn't just useful inside the browser. It could be useful inside the terminal too. An AI memory layer that your code agent can query. "What did I discuss about authentication last week?" The answer comes from your web AI chat history.
I built something for that. More in the next article.
If you hit "Restore Button Does Nothing," the troubleshooting guide covers the most common fixes.