Fix ChatGPT composer-pill DOM rewrite (model + thinking-effort)#146
Fix ChatGPT composer-pill DOM rewrite (model + thinking-effort)#146SyntaxSmith wants to merge 1 commit into
Conversation
ChatGPT replaced the composer's model picker and thinking-effort chip with a pair of __composer-pill buttons, and the per-effort selector moved into a per-row trailing button inside the model menu. - Extend MODEL_BUTTON_SELECTOR with a fallback for the new pill so model selection works on both the legacy testid and the rewritten composer. - Rewrite buildThinkingTimeExpression to navigate the new data-model-picker-thinking-effort-action trailing button (resolving the submenu via aria-controls) and match effort labels in both English and Chinese, with the old composer-chip flow kept as a fallback for accounts still on the previous UI. - Downgrade ensureThinkingTime to log a debug DOM dump and return on any not-found state instead of throwing, so Pro consults survive further UI shifts on whatever effort ChatGPT defaults to.
There was a problem hiding this comment.
Pull request overview
Updates the browser automation selectors/flows to keep ChatGPT “browser mode” working after the April 2026 composer DOM rewrite, specifically around model selection and “Thinking effort” selection.
Changes:
- Expand the model picker button selector to match both the legacy
data-testidbutton and the new__composer-pillbutton. - Rework thinking-effort selection to navigate the new model-menu “trailing effort” button (with EN/中文 label token matching) while retaining the legacy chip flow as a fallback.
- Change
ensureThinkingTimeto log debug context and continue with ChatGPT defaults instead of throwing on missing UI elements.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| src/browser/constants.ts | Broadens the model button selector to include the new composer pill. |
| src/browser/actions/thinkingTime.ts | Implements new thinking-effort selection path via model menu + adds best-effort non-throwing behavior. |
| CHANGELOG.md | Documents the browser automation fix for the composer rewrite. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export const MODEL_BUTTON_SELECTOR = | ||
| '[data-testid="model-switcher-dropdown-button"], button.__composer-pill[aria-haspopup="menu"]'; |
There was a problem hiding this comment.
MODEL_BUTTON_SELECTOR is now a comma-separated selector list. document.querySelector() returns the first match in document order across either branch, so if any other button.__composer-pill[aria-haspopup="menu"] appears before the legacy [data-testid="model-switcher-dropdown-button"], callers can start clicking the wrong button (even on accounts where the legacy testid still exists). Consider replacing this constant with an ordered lookup (try legacy selector first, then the new pill), or otherwise narrowing the pill selector so it only matches the model picker.
| export const MODEL_BUTTON_SELECTOR = | |
| '[data-testid="model-switcher-dropdown-button"], button.__composer-pill[aria-haspopup="menu"]'; | |
| export const LEGACY_MODEL_BUTTON_SELECTOR = | |
| '[data-testid="model-switcher-dropdown-button"]'; | |
| export const FALLBACK_MODEL_BUTTON_SELECTOR = | |
| 'button.__composer-pill[aria-haspopup="menu"]'; | |
| export const MODEL_BUTTON_SELECTORS = [ | |
| LEGACY_MODEL_BUTTON_SELECTOR, | |
| FALLBACK_MODEL_BUTTON_SELECTOR, | |
| ] as const; | |
| export const MODEL_BUTTON_SELECTOR = LEGACY_MODEL_BUTTON_SELECTOR; |
| const OLD_CHIP_SELECTORS = [ | ||
| '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]', | ||
| '.__composer-pill-composite button[aria-haspopup="menu"]', | ||
| ]; | ||
| const findOldChip = () => { | ||
| for (const selector of OLD_CHIP_SELECTORS) { | ||
| for (const btn of document.querySelectorAll(selector)) { | ||
| if (btn.getAttribute?.('aria-haspopup') !== 'menu') continue; | ||
| // The new model picker pill also reuses .__composer-pill — skip it. | ||
| if (btn.matches?.(MODEL_BUTTON_SELECTOR)) continue; | ||
| const aria = normalize(btn.getAttribute?.('aria-label') ?? ''); | ||
| const text = normalize(btn.textContent ?? ''); | ||
| if (aria.includes('thinking') || text.includes('thinking')) { | ||
| return btn; | ||
| } | ||
|
|
||
| // In some cases the pill is labeled "Pro". | ||
| if (aria.includes('pro') || text.includes('pro')) { | ||
| return btn; | ||
| } | ||
| if (aria.includes('thinking') || text.includes('thinking')) return btn; | ||
| } |
There was a problem hiding this comment.
findOldChip() currently skips any button matching MODEL_BUTTON_SELECTOR. Since MODEL_BUTTON_SELECTOR now includes button.__composer-pill[aria-haspopup="menu"], this can also exclude the legacy "Thinking" chip in UIs where it used the __composer-pill class (which the old implementation explicitly handled). That would prevent the fallback path from ever running and silently degrade to default effort even though the old chip is present. Consider restoring a selector path for button.__composer-pill while disambiguating by label/text (e.g., require "thinking" tokens) rather than excluding via MODEL_BUTTON_SELECTOR.
| const modelBtn = findModelButton(); | ||
| if (!modelBtn) { | ||
| return { status: 'chip-not-found' }; | ||
| } | ||
|
|
||
| dispatchClickSequence(chip); | ||
| // Open model menu (idempotent — leaves it open if already open). | ||
| if (modelBtn.getAttribute('aria-expanded') !== 'true') { | ||
| dispatchClickSequence(modelBtn); | ||
| await sleep(INITIAL_WAIT_MS); | ||
| } | ||
|
|
||
| return new Promise((resolve) => { | ||
| const start = performance.now(); | ||
| let trailing = null; | ||
| const trailingDeadline = performance.now() + MAX_WAIT_MS; | ||
| while (performance.now() < trailingDeadline) { | ||
| trailing = pickTrailingForCurrentModel(); | ||
| if (trailing) break; | ||
| await sleep(100); | ||
| } | ||
| if (!trailing) { | ||
| closeOpenMenus(); | ||
| return { status: 'chip-not-found' }; | ||
| } |
There was a problem hiding this comment.
The expression returns { status: 'chip-not-found' } for multiple different failure modes (no model button, no trailing effort button). This makes logs/debug dumps harder to interpret and can hide which selector actually broke. Consider adding distinct statuses (e.g. model-button-not-found, effort-button-not-found) and mapping them to clearer log contexts in ensureThinkingTime.
| // Bilingual matchers: English level token + observed Chinese variants. | ||
| const LEVEL_TOKENS = { | ||
| light: ['light', '轻'], | ||
| standard: ['standard', '标准'], | ||
| extended: ['extended', '扩展', '深度', '加强'], | ||
| heavy: ['heavy', '重度', '加重', '高'], | ||
| }; | ||
| const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL]; |
There was a problem hiding this comment.
There are existing unit tests for the generated browser expression (tests/browser/thinkingTime.test.ts), but they don’t currently assert the new behavior introduced here (MODEL_BUTTON_SELECTOR inclusion and bilingual LEVEL_TOKENS). Adding assertions for these literals would help catch future UI selector regressions without needing a live smoke test.
|
Thank you for this. Do you know how to have it select Configure so that older models like 5.4 Pro can be used? I wasn't able to get that working. |
No,I have no idea. Maybe you can choose it manually and this tool will use it by default |
|
Superseded by #158, which landed the focused ChatGPT composer-pill model selector and thinking-effort menu fix from this PR, with updated tests and CHANGELOG credit. Thanks @SyntaxSmith. |
Summary
Restores browser mode after ChatGPT's recent composer rewrite:
Without this, every browser-mode consult fails at "Unable to locate the ChatGPT model selector button" (or, after working around that, at "Unable to find the Thinking chip button in the composer area").
Test plan