📝 checkpoint.mdv4.5.1 · 2026-10-02

Checkpoint: saving a claude.ai conversation to PDF via a DevTools snippet (page crash / one-line print preview)


1. Problem & Resolution

Goal

Andrew keeps an independent, verbatim transcript of every AI conversation (AIs eventually paraphrase, embellish, censor or summarise despite instructions). His method is: open Chrome DevTools on claude.ai, paste a snippet that expands the chat so the browser's Print to PDF captures the whole conversation, then press Ctrl+P.

Problem

Environment (as observed)

What was tried, in order

# Time (AEDT) Attempt Reported outcome
1 03:57 Andrew's original snippet: finds the largest overflow-y:auto div, hides all siblings up the ancestor chain, marks the chain data-pf, injects CSS forcing height:auto, overflow:visible, contain:none, content-visibility:visible. Crashes when the chat has a graphic (Andrew's report).
2 03:57 Reply 1: a count diagnostic, then a cleanup block (replace iframes/canvas/video with a placeholder, cap img/svg size), then the original snippet. "that crashes too" (04:19). Diagnostic output returned: iframe 0, canvas 0, svg 1, img 0, video 0.
3 04:20 Reply 2: staged test (run only the hide-siblings half, then the style half without contain and content-visibility) plus a size diagnostic. Staged test result not reported. A screenshot of the size diagnostic came back (04:35): only page furniture, i.e. the 32x32 Claude "Spark" logo svg, a hidden analytics iframe (isolated-segment.html, display:none) and a hidden 1x1 about:blank iframe.
4 04:35 Reply 3: refined diagnostic listing only elements larger than 100x100. Andrew: it "only just crashed a few moments ago", i.e. the crash also happened in a chat with no content graphic (implied: the Ship of Fools chat).
5 04:37 Reply 4: stop altering the live page; copy the scroller's innerHTML plus the page stylesheets into a new window and print that. No crash, but the print preview showed only one line at the top.
6 04:43 Reply 5: same new-window idea, but fetch the stylesheet text, neutralise every @media print, and reset inline position/transform/height in the copy. Built to tell print-CSS apart from positioning as the cause. Not reported. Andrew replied with frustration and asked for a quick one-fits-all fix.
7 04:44 Reply 6: plain-text fallback, copies sc.innerText into a <pre> in a new window. Not reported. Andrew hard-refreshed (Ctrl+F5) and proposed stripping or greying the graphics.
8 05:23 Reply 7: block that hides every graphic inside the chat (leaving a [graphic omitted] marker), to be pasted before "the old way". Andrew's version "displays the graphics again". He pasted his own three-part script (strip block, an overflow loop over every element, an @media print style).
9 05:46 Reply 8: single script, graphics hidden by an injected stylesheet rule so a page redraw cannot bring them back. Andrew: "3 scripts, actually".
10 05:46 Reply 9: final script merging all three of his parts plus the stylesheet rule (section 3). Not yet reported.
11 06:12 to 06:18 Transcript PDF and MD built and delivered. Delivered.

Findings

Observed (from Andrew's DevTools output):

Hypotheses, not verified:

Known good:

Resolution state

Proposal pending: the final script in section 3. Expected effect: all graphics inside the chat hidden, [graphic omitted] markers shown where they were, every container expanded, print preview showing the full conversation. Not yet confirmed.


2. Risk Analysis

Risk Detail
Tab crash again The final script still runs a loop over document.querySelectorAll('*') that forces overflow/height on every element, the same class of layout forcing suspected earlier. It could crash on long chats. Cost: tab only, nothing is lost.
Wrong region picked The scroller is "the largest overflow-y:auto div by scrollHeight". If another element wins (sidebar, a scrolling code block), the wrong region is tagged. If none matches, root is undefined and the script throws a TypeError.
Order dependence The scroller must be found before the loop, because the loop sets overflow:visible everywhere and would stop it being found. The script already does this; do not reorder.
Graphics not caught Hiding is by selector: iframe, canvas, video, object, embed, img, svg (svg inside a button or a is kept on purpose, to leave icons alone). A graphic drawn another way (CSS background, shadow DOM, an svg wrapped in a link or button) would still show.
Markers unreliable [graphic omitted] markers are inserted by script and may be removed if the page redraws. The stylesheet rule hides the graphic regardless.
Print preview still truncated The cause of the one-line preview is unresolved (print CSS vs positioning). The final script sets @media print overrides for overflow/height but does not disable the page's other print rules.
Print only captures what is rendered If old messages are not in the DOM, they will not print. This is the main limit of the whole snippet approach for the "verbatim" goal.
Transcript made here is not independent The PDF/MD in this conversation were produced from this session's own context, not from the page. Structure was checked (20 turns, balanced code fences, 2 embedded images, no blank pages), but the wording was copied by the model. Andrew's own capture remains the authority.
Tool rows and saved preferences omitted The "Get current Sydney time" tool rows and the saved-preferences text were left out of the transcript on purpose, since they are not chat messages.

3. Code Change

No repository is involved. The "code" is a console script. Diff of the final script against Andrew's last pasted version (user_script_U9.js to final_script_A10.js; timestamps in the diff header removed):

--- user_script_U9.js
+++ final_script_A10.js
@@ -1,5 +1,6 @@
{
const root = [...document.querySelectorAll('div')].filter(e => getComputedStyle(e).overflowY === 'auto' && e.scrollHeight > e.clientHeight).sort((a,b) => b.scrollHeight - a.scrollHeight)[0];
+ root.setAttribute('data-gx','1');
let hidden = 0;
root.querySelectorAll('iframe,canvas,video,object,embed,img,svg').forEach(e => {
if (e.closest('button,a,[role=button]')) return;
@@ -20,4 +21,4 @@
el.style.maxHeight = 'none';
}
});
-document.head.appendChild(Object.assign(document.createElement('style'),{textContent:'@media print{*{overflow:visible!important;height:auto!important;max-height:none!important}}'}))
+document.head.appendChild(Object.assign(document.createElement('style'),{textContent:'[data-gx] iframe,[data-gx] canvas,[data-gx] video,[data-gx] object,[data-gx] embed,[data-gx] img,[data-gx] svg:not(button svg):not(a svg){display:none!important}@media print{*{overflow:visible!important;height:auto!important;max-height:none!important}}'}))

Meaning of the two changes:

Final script (as delivered, paste as one block, then Ctrl+P)

{
const root = [...document.querySelectorAll('div')].filter(e => getComputedStyle(e).overflowY === 'auto' && e.scrollHeight > e.clientHeight).sort((a,b) => b.scrollHeight - a.scrollHeight)[0];
root.setAttribute('data-gx','1');
let hidden = 0;
root.querySelectorAll('iframe,canvas,video,object,embed,img,svg').forEach(e => {
if (e.closest('button,a,[role=button]')) return;
if (e.parentElement && e.parentElement.closest('svg')) return;
const m = document.createElement('div');
m.textContent = '[graphic omitted]';
e.parentNode.insertBefore(m, e);
e.style.setProperty('display','none','important');
hidden++;
});
console.log('graphics hidden:', hidden);
}
document.querySelectorAll('*').forEach(el => {
const s = getComputedStyle(el);
if (s.overflowY !== 'visible' || s.position === 'fixed' || el.scrollHeight > el.clientHeight) {
el.style.overflow = 'visible';
el.style.height = 'auto';
el.style.maxHeight = 'none';
}
});
document.head.appendChild(Object.assign(document.createElement('style'),{textContent:'[data-gx] iframe,[data-gx] canvas,[data-gx] video,[data-gx] object,[data-gx] embed,[data-gx] img,[data-gx] svg:not(button svg):not(a svg){display:none!important}@media print{*{overflow:visible!important;height:auto!important;max-height:none!important}}'}))

The console prints graphics hidden: N. N = 0 on a chat that has graphics means the selector missed them.

Fallback A: plain text only (cannot fail on layout, loses all formatting and graphics)

const sc = [...document.querySelectorAll('div')].filter(e => getComputedStyle(e).overflowY === 'auto' && e.scrollHeight > e.clientHeight).sort((a,b) => b.scrollHeight - a.scrollHeight)[0];
const w = window.open('', '_blank');
w.document.write('<!doctype html><meta charset="utf-8"><title>chat</title><pre style="white-space:pre-wrap;font:14px/1.5 Georgia,serif;max-width:800px;margin:auto"></pre>');
w.document.close();
w.document.querySelector('pre').textContent = sc.innerText;

Diagnostic: find the real graphics (scroll the graphic into view first, then run)

[...document.querySelectorAll('svg,iframe,canvas,img,video')].forEach(e => { const r = e.getBoundingClientRect(); if (r.width > 100 && r.height > 100) console.log(e.tagName, Math.round(r.width) + 'x' + Math.round(r.height), e); });

Other variants (staged test, new-window copy, fetch-and-neutralise-print-CSS) are in the transcript, in the replies stamped 04:20:14, 04:37:10 and 04:43:06.


4. If the proposal fails: next steps, in order

  1. Record which symptom it is: (a) tab crash, (b) blank or one-line print preview, (c) graphics still visible, (d) preview missing early messages. The fix differs for each.
  2. (a) Crash: drop the blanket querySelectorAll('*') loop. Run the staged test from the reply stamped 04:20:14 (hide siblings only, then the style half without contain and content-visibility) to find which stage kills the tab.
  3. (b) One line or blank preview: run the new-window variant from the reply stamped 04:43:06 and look at the new window on screen before pressing Ctrl+P. All messages visible on screen means print CSS; one line on screen as well means positioning.
  4. (c) Graphics visible: scroll to the graphic, run the size diagnostic above, paste what it prints. The element type goes into the hide list.
  5. (d) Missing messages: the page does not hold them in the DOM, so no print snippet will capture them. Use the data export route below.
  6. Robust alternative that avoids the browser page entirely: claude.ai's Settings, Privacy, "Export data" (an emailed download of all conversations as JSON; this comes from general knowledge, not checked in this session). A converter from that JSON to Markdown and PDF in the style used here would then be written. That needs a small sample of the exported JSON, not the whole export.
  7. Last resort: fallback A (plain text).

5. Artefacts

6. Open items