--- image-use.orig 2026-09-29 22:36:16
+++ image-use 2026-09-29 22:48:46
@@ -1380,7 +1380,7 @@
_JS_STATE = r"""(() => {
const seen = new Set(%s);
const stop = !!document.querySelector(
- 'button[data-testid="stop-button"], button[aria-label*="Stop" i]'
+ 'button[data-testid="stop-button"], button[aria-label*="Stop" i], button[aria-label*="停止"]'
);
const re = new RegExp(%s);
// The generated image renders in <main> but OUTSIDE any message bubble (it's
@@ -1389,12 +1389,25 @@
// user bubble with a fresh estuary/content src not in the pre-submit baseline.
// So detect fresh main images but exclude any that belong to a user message.
const userSrcs = new Set();
- document.querySelectorAll('[data-message-author-role="user"] img')
+ document.querySelectorAll('[data-message-author-role="user"] img, [data-user-message-bubble] img')
.forEach(i => { const s = i.currentSrc || i.src; if (s) userSrcs.add(s); });
+ // 2026-09 ChatGPT redesign: results render as blob: imgs inside
+ // [data-testid=generated-image-preview]; accept those alongside the regex.
const fresh = [...document.querySelectorAll('main img')]
+ .filter(i => re.test(i.currentSrc || i.src) ||
+ (!!i.closest('[data-testid="generated-image-preview"]') && i.complete && i.naturalWidth > 0))
.map(i => i.currentSrc || i.src)
- .filter(s => re.test(s) && !seen.has(s) && !userSrcs.has(s));
- const a = document.querySelectorAll('[data-message-author-role="assistant"]');
+ .filter(s => s && !seen.has(s) && !userSrcs.has(s));
+ let a = [...document.querySelectorAll('[data-message-author-role="assistant"]')];
+ if (!a.length) {
+ // Redesign: each [data-turn-key] holds the user bubble AND the reply; the
+ // reply counts once it has rendered content (markdown or an image gallery).
+ const turns = [...document.querySelectorAll('main [data-turn-key]')];
+ const t = turns[turns.length - 1];
+ const body = t && t.querySelector('[data-conversation-role="assistant"]') &&
+ t.querySelector('[data-markdown-copy], [data-testid="generated-image-gallery"]');
+ if (body) a = [body];
+ }
const lastA = a[a.length - 1];
const dlg = [...document.querySelectorAll('[role="dialog"]')]
.map(d => d.textContent || '').join(' ');
@@ -1407,7 +1420,9 @@
_JS_BASELINE = r"""(() => {
const re = new RegExp(%s);
const srcs = [...document.querySelectorAll('main img')]
- .map(i => i.currentSrc || i.src).filter(s => re.test(s));
+ .filter(i => re.test(i.currentSrc || i.src) ||
+ !!i.closest('[data-testid="generated-image-preview"]'))
+ .map(i => i.currentSrc || i.src).filter(Boolean);
return JSON.stringify(srcs);
})()"""
@@ -1476,7 +1491,7 @@
const dlg = [...document.querySelectorAll('[role="dialog"]')]
.map(d => d.textContent || '').join(' ');
return JSON.stringify({
- composer: !!document.querySelector('#prompt-textarea'),
+ composer: !!document.querySelector('#prompt-textarea, div.ProseMirror[contenteditable=true]'),
limited: /too many requests|requests too quickly/i.test(dlg),
});
})()"""
@@ -2130,7 +2145,7 @@
# spacing) or textContent (which loses paragraph breaks). A ProseMirror trailing
# BR is an empty-paragraph placeholder, not an extra newline.
_JS_WEB_COMPOSER_STATE = r"""(() => {
- const composer = document.querySelector('#prompt-textarea');
+ const composer = document.querySelector('#prompt-textarea, div.ProseMirror[contenteditable=true]');
const form = composer && composer.closest('form');
const read = n => {
if (n.nodeType === 3) return n.textContent;
@@ -2147,7 +2162,7 @@
}
const images = form ? [...form.querySelectorAll('img')]
.filter(i => /blob:|estuary\/content|oaiusercontent/.test(i.src || '')) : [];
- const send = form && form.querySelector('button[data-testid="send-button"]');
+ const send = form && form.querySelector('button[data-testid="send-button"], button[type=submit]');
return JSON.stringify({
exists: !!composer, text,
attachments: images.length,
@@ -2156,7 +2171,11 @@
ready: images.filter(i => !i.src.startsWith('blob:')).length,
busy: !!(form && form.querySelector('[role="progressbar"], [aria-busy="true"]')),
send_ready: !!send && !send.disabled && send.getAttribute('aria-disabled') !== 'true',
- user_turns: document.querySelectorAll('[data-message-author-role="user"]').length,
+ user_turns: document.querySelectorAll('[data-message-author-role="user"]').length +
+ new Set([...document.querySelectorAll('[data-user-message-bubble]')].map((b, i) => {
+ const t = b.closest('[data-turn-key]');
+ return t ? t.getAttribute('data-turn-key') : 'b' + i;
+ })).size,
});
})()"""
@@ -2253,7 +2272,7 @@
def _clear_web_composer(ab, session, remaining) -> None:
"""Best-effort: empty the composer so our own text is not left as a draft."""
try:
- _ab(ab, "fill", "#prompt-textarea", "--stdin", input_text="",
+ _ab(ab, "fill", "#prompt-textarea, div.ProseMirror[contenteditable=true]", "--stdin", input_text="",
session=session, timeout=min(20.0, remaining()))
except GatewayError:
pass
@@ -2275,7 +2294,7 @@
if initial.get("text"):
raise GatewayError("ChatGPT composer is not empty; prompt not submitted. " + _DRAFT_HINT)
emit("pasting prompt")
- _ab(ab, "fill", "#prompt-textarea", "--stdin", input_text=user_text,
+ _ab(ab, "fill", "#prompt-textarea, div.ProseMirror[contenteditable=true]", "--stdin", input_text=user_text,
session=session, timeout=min(60.0, remaining()))
# Everything below until the click is a "nothing was sent" failure. ChatGPT
# keeps composer drafts across chats, so leaving our text in the box would
@@ -2309,7 +2328,7 @@
emit(f"verified prompt ({len(user_text)} characters) and {n_refs} reference image(s)")
send_error = None
try:
- _ab(ab, "click", 'button[data-testid="send-button"]',
+ _ab(ab, "click", 'form button[data-testid="send-button"], form button[type=submit]',
session=session, timeout=min(20.0, remaining()))
except GatewayError as error:
send_error = error
Version: image-use 0.30.0, chrome-use 1.5.144, macOS, Chinese UI, Plus account, relay connected (
doctorall ok for web).Command
Symptoms (in order, each surfaced after fixing the previous one)
composer never appeared (likely a login wall …)— page is logged in, but the ProseMirror composer no longer hasid="prompt-textarea"(it is nowdiv.ProseMirror[contenteditable=true]inside aform).ChatGPT Send button did not become ready— send button no longer hasdata-testid="send-button"; it is a plainbutton[type=submit]witharia-label="发送"inside the composer form.could not confirm ChatGPT received the prompt— the prompt was sent (conversation created), but[data-message-author-role]no longer exists anywhere in the DOM.<img src="blob:https://chatgpt.com/…">insidebutton[data-testid="generated-image-preview"](under[data-testid="generated-image-gallery"]), soWEB_IMG_SRC_REnever matches.New DOM observations (2026-09-29)
div[data-turn-key=…]containing both the user bubble ([data-user-message-bubble=true]) and an assistant headingh4[data-conversation-role=assistant](present before the reply renders, so it's not a "reply done" signal on its own).data-turn-keyappears 2×), so naive counts are doubled — dedupe bydata-turn-key.fetch(blobSrc)works fine fromchrome-use eval(gotimage/png, 2.1 MB), so_JS_FETCHneeds no change.button[aria-label*="停止"](localized UI).warning: model picker not found) — harmless, not investigated.Local patch that makes it work end-to-end (verified: submit → generating → receiving image → download → conversation deleted, ~105 s):
Selector fallbacks only; old selectors kept so the old DOM still works.
image-use-chatgpt-redesign.patch
image-use doctoroutput: