Skip to content

fix(chatgpt): use data-turn to detect upload previews vs generated images - #2292

Open
jesuisjohan wants to merge 1 commit into
jackwener:mainfrom
jesuisjohan:fix/chatgpt-multi-image-upload-preview-detection
Open

fix(chatgpt): use data-turn to detect upload previews vs generated images#2292
jesuisjohan wants to merge 1 commit into
jackwener:mainfrom
jesuisjohan:fix/chatgpt-multi-image-upload-preview-detection

Conversation

@jesuisjohan

Copy link
Copy Markdown

Bug

opencli chatgpt image <prompt> --image a.png,b.png,c.png (2+ reference
images) can silently return the just-uploaded reference thumbnails instead
of the actual generated/edited image. With a single --image it works
correctly.

Root cause

isUserUploadPreview() in clis/chatgpt/utils.js (used by
getChatGPTVisibleImageUrls/waitForChatGPTImages to exclude a user's own
attachment 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 .textContent correctly
    reads the heading. jsdom's innerText is always undefined, so the
    existing 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 a
    multi-file attachment reads "Open image N of M: <name>", which no
    longer 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'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 once
multiple attachments are involved, so it returns 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. The
heading/aria-label checks stay as a fallback (now using textContent and
a substring aria-label match) for markup that lacks data-turn.

Verification

  • Confirmed the real DOM shape live against chatgpt.com (data-turn on
    the turn section, empty innerText on the heading, "Open image N of M"
    aria-label) via opencli browser <session> eval.
  • Reproduced the bug live: 3 attached reference images + a real generation
    prompt returned the 3 uploaded thumbnails as "generated" output.
  • Applied this fix to the installed build and re-ran the identical
    request: it now returns exactly one file, the real generated/composed
    image.
  • Added two regression tests reproducing the exact failure (multi-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

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant