fix(chatgpt): use data-turn to detect upload previews vs generated images - #2292
Open
jesuisjohan wants to merge 1 commit into
Open
Conversation
…ages
`chatgpt image` with 2+ --image attachments could return the just-uploaded
reference thumbnails instead of the actual generated image.
isUserUploadPreview() classified an <img> as a user upload (to exclude it
from waitForChatGPTImages' before/after diff) using two signals, both
broken against ChatGPT's current DOM:
- turn.querySelector('h4')?.innerText: the heading is visually hidden, so
real Chrome's innerText resolves to '' (layout-dependent) even though
.textContent correctly reads "You said:" / "ChatGPT said:". jsdom's
innerText is always undefined, so the test suite never exercised this
path either - it happened to pass via the aria-label/alt fallback below.
- button[aria-label^="Open image:"]: ChatGPT's current label for a
multi-file attachment reads "Open image N of M: <name>", which no
longer starts with "Open image:", so this selector stopped matching.
With both signals dead, classification fell through to alt-text sniffing.
Right after upload, an attachment thumbnail's alt/aria-label haven't
populated yet, so for a poll or two every uploaded image is misclassified
as "new". waitForChatGPTImages returns as soon as two consecutive polls
agree on a URL set - long enough for that transient window to win when
multiple attachments are involved, so it can return the uploads instead of
the real result.
Fix: check the turn <section>'s own data-turn="user"|"assistant"
attribute first. It's set structurally as soon as the turn mounts, not
tied to the attachment's async metadata, so it isn't subject to the race.
Keep the heading/aria-label checks as a fallback (now using textContent
and a substring aria-label match) for markup that lacks data-turn.
Verified live against chatgpt.com: reproduced the bug with 3 reference
images, then confirmed the patched build returns exactly the one real
generated image instead of the 3 uploaded thumbnails.
Adds regression tests for both the data-turn race and the aria-label
format change; confirmed both fail against the pre-fix code.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L29nrhaeQ4W5rjNr27z47h
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Bug
opencli chatgpt image <prompt> --image a.png,b.png,c.png(2+ referenceimages) can silently return the just-uploaded reference thumbnails instead
of the actual generated/edited image. With a single
--imageit workscorrectly.
Root cause
isUserUploadPreview()inclis/chatgpt/utils.js(used bygetChatGPTVisibleImageUrls/waitForChatGPTImagesto exclude a user's ownattachment previews from the "new image appeared" diff) relied on two
signals that no longer match ChatGPT's current DOM:
turn.querySelector('h4')?.innerText— the turn heading ("You said:" /"ChatGPT said:") is visually hidden, so real Chrome's
innerText(layout-dependent) resolves to
'', even though.textContentcorrectlyreads the heading. jsdom's
innerTextis alwaysundefined, so theexisting test suite never caught this either — it happened to keep
passing via the aria-label/alt fallback below.
button[aria-label^="Open image:"]— ChatGPT's current label for amulti-file attachment reads
"Open image N of M: <name>", which nolonger starts with
"Open image:", so the selector stopped matching.With both signals dead, classification falls through to alt-text sniffing.
Right after upload, an attachment thumbnail's
alt/aria-label haven'tpopulated yet, so for a poll or two every uploaded image is misclassified
as "new".
waitForChatGPTImagesreturns as soon as two consecutive pollsagree on a URL set — long enough for that transient window to win once
multiple attachments are involved, so it returns the uploads instead of
the real result.
Fix
Check the turn
<section>'s owndata-turn="user"|"assistant"attributefirst. It's set structurally as soon as the turn mounts — not tied to the
attachment's async metadata — so it isn't subject to the race. The
heading/aria-label checks stay as a fallback (now using
textContentanda substring aria-label match) for markup that lacks
data-turn.Verification
data-turnonthe turn section, empty
innerTexton the heading, "Open image N of M"aria-label) via
opencli browser <session> eval.prompt returned the 3 uploaded thumbnails as "generated" output.
request: it now returns exactly one file, the real generated/composed
image.
upload racing before alt/aria-label populate, and the "Open image N of
M" aria-label format) — both fail against the pre-fix code and pass
after the fix.
npx vitest run clis/chatgpt/utils.test.js clis/chatgpt/image.test.js→128/128 passing.
🤖 Generated with Claude Code