Repository navigation
fix(annotate): pins on images without an id restore by their source - #1747
Conversation
A pinpoint on an <img>, <video> or frame with no id and no data-* identity got no anchor, so its pin never came back after a reload or an HTML refresh. Screenshots and charts in generated pages rarely carry an id, so in practice image comments did not persist. The rule comes from D1 in backnotprop#1251: a structure-derived signature is the same for identical siblings, so a positional selector could bind the wrong one after a sibling moved. A media element's source is not structure: two siblings showing different files never share it. The snapshot for a text-less media element is now src:<key>, where the key is the author's reference to the resource: relative to the document's base when it lies under it, otherwise origin and path, never the query or the fragment. A data: or blob: source, or a source that another element of the same kind shows, gives no snapshot, so identical siblings still fail closed. When the positional selector no longer lands on the element, restore finds it again by its source, and two matches resolve nothing. AGENTS.md said text-less pins restore through their anchor, which held only with an id or a data-* attribute; it now says when they do. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dropping the query let a pin bind to a different image that shares a path: /_next/image?url=a.jpg and ?url=b.jpg, /thumb?id=1 and ?id=2, and proto.html?step=1 and ?step=2 all read as one source. The query and fragment now count as an 8-hex FNV-1a digest, so a token never lands in the annotation in plain text and a changed query fails closed. Same-origin sources outside the base key by root-relative path (a live app's proxy port changes per session), and a key past the cap keeps a digest of the whole. Docs: the embed paragraph described the old fail-closed rule for frames without an id; it now says when a frame pin restores. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks @pro-vi, for finding this and for the fix. Comments on images without an id going missing after a reload was a real gap, especially in generated reports. We added one commit on top of yours: the source key now includes the part of the URL after We checked it in a real browser: a pin on an id-less image comes back after a reload, in a new session, after another image is added before it, and in live-app sessions. A pin on an image that appears twice isn't saved there, so it can't jump to the copy. It ships in 0.28.7. |
Problem
In raw-HTML annotate, a pin on an
<img>,<video>or frame that has noidand nodata-*identity attribute does not come back after a reload or an HTML refresh. The pin is saved without an anchor, and its quote ([element: Image (chart.png)]) appears nowhere in the page, so restore finds nothing. Images in generated reports and docs rarely carry an id, so in practice image comments don't persist.The existing test "an image pin saved with the old bare placeholder still restores through its anchor" passes because its image has
id="hero".We hit this in artifact-server's review frame: every image comment was stored as
originalText: "[element: Image]"withhtmlAnchor: null.Why it was this way
buildElementAnchorgives a text-less element an anchor only through#idor adata-*rung. That came from D1 in #1251: a shape signature (tag, classes, child count, size) is the same for identical siblings, so after one is removed a positional selector lands on the other and the signature still verifies.What changed
anchorTextSnapshotreturnssrc:<key>fromanchorSourceSnapshotwhen the element has no text. The source comes fromdescRawSource(src, lazydata-src,srcset,<picture><source>, a video's<source>orposter). The key is the author's reference to the resource: relative to the document's base when the resource lies under it, otherwise origin + path. The query and fragment are dropped, as inctxScrubUrl: they carry per-visit state and should not end up in a stored annotation.<base href="/api/html-assets/<token>/">,charts/b.pngkeys ascharts/b.png, not the token route.data:/blob:source, or when another element of the same kind shows the same source. Siblings with different files can't stand in for each other, and the same file twice is refused.relocateBySourcefinds the one element with that source. Two matches resolve nothing. Text pins already fall back to text search; this is the media equivalent.The stored anchor keeps its shape (
{selector, tagName, text}), andtextstays within 180 characters, underMAX_HTML_ANCHOR_TEXT_LENGTH. No protocol change. Pins saved before this have no anchor and still won't restore; there is nothing to migrate.Tests
New in
srcdoc.test.ts: "a media pin without stable identity restores by its source; shared and inline sources fail closed". It pins an id-less image whosesrchas a signed query, restores it, restores it again after a sibling is inserted in front, gets nothing once that source is gone, and gets no anchor for a shared ordata:source. A crafted anchor naming the shared source binds nothing.mainthe test fails at the first check (the anchor isundefined).data:/blob:exclusion, the two-matches rule in relocation, and relocation itself.DOM_TESTS=1 bun test packages/ui/components/html-viewer(258 pass),packages/ui/utilsandpackages/ui/hookspass;tsc --noEmit -p packages/ui/tsconfig.jsonis clean.Not covered
bun test packages/ui packages/corerun crashed Bun itself (segfault), so I ran the folders separately.DOM_TESTS=1, three "live paste service E2E" tests inpackages/core/crypto.test.tsfail. They fail the same way onmainand pass without the DOM preload.annotate-app) pages, and HTML Refresh end to end. Relocation applies to both, but the test drives it throughfind-and-markonly.🤖 Generated with Claude Code