Skip to content

Commit 60a8a8a

Browse files
committed
fix(browser): handle Extended Pro fresh-chat selector
1 parent ab116c0 commit 60a8a8a

6 files changed

Lines changed: 78 additions & 26 deletions

File tree

src/browser/actions/domEvents.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,28 @@ export function buildClickDispatcher(functionName = "dispatchClickSequence"): st
66
if(!target || !(target instanceof EventTarget)) return false;
77
const types = ${typesLiteral};
88
for (const type of types) {
9-
const common = { bubbles: true, cancelable: true, view: window };
9+
const common = {
10+
bubbles: true,
11+
cancelable: true,
12+
composed: true,
13+
view: window,
14+
button: 0,
15+
buttons: 1,
16+
clientX: 10,
17+
clientY: 10,
18+
screenX: 10,
19+
screenY: 10,
20+
detail: 1,
21+
};
1022
let event;
1123
if (type.startsWith('pointer') && 'PointerEvent' in window) {
12-
event = new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' });
24+
event = new PointerEvent(type, {
25+
...common,
26+
pointerId: 1,
27+
pointerType: 'mouse',
28+
isPrimary: true,
29+
pressure: type === 'pointerup' ? 0 : 0.5,
30+
});
1331
} else {
1432
event = new MouseEvent(type, common);
1533
}

src/browser/actions/modelSelection.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -139,31 +139,57 @@ function buildModelSelectionExpression(
139139
};
140140
141141
const getButtonLabel = () => (button.textContent ?? '').trim();
142-
if (MODEL_STRATEGY === 'current') {
143-
return { status: 'already-selected', label: getButtonLabel() };
144-
}
145-
const buttonMatchesTarget = () => {
146-
const normalizedLabel = normalizeText(getButtonLabel());
142+
const getSelectedModelLabels = () => {
143+
const labels = new Set();
144+
const push = (value) => {
145+
const normalized = (value ?? '').trim();
146+
if (normalized) {
147+
labels.add(normalized);
148+
}
149+
};
150+
push(getButtonLabel());
151+
const chipSelectors = [
152+
'[aria-label*="click to remove"]',
153+
'[class*="composer-pill"]',
154+
'[data-testid*="composer-pill"]',
155+
];
156+
for (const selector of chipSelectors) {
157+
const nodes = Array.from(document.querySelectorAll(selector));
158+
for (const node of nodes) {
159+
push(node.getAttribute?.('aria-label')?.replace(/,\\s*click to remove$/i, ''));
160+
push(node.textContent);
161+
}
162+
}
163+
return Array.from(labels);
164+
};
165+
const labelMatchesTarget = (label) => {
166+
const normalizedLabel = normalizeText(label);
147167
if (!normalizedLabel) return false;
148168
if (desiredVersion) {
149169
if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
150170
if (desiredVersion === '5-2' && !normalizedLabel.includes('5 2')) return false;
151171
if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
152172
if (desiredVersion === '5-0' && !normalizedLabel.includes('5 0')) return false;
153173
}
154-
if (wantsPro && !normalizedLabel.includes(' pro')) return false;
174+
if (wantsPro && !normalizedLabel.includes('pro')) return false;
155175
if (wantsInstant && !normalizedLabel.includes('instant')) return false;
156176
if (wantsThinking && !normalizedLabel.includes('thinking')) return false;
157-
// Also reject if button has variants we DON'T want
158177
if (!wantsPro && normalizedLabel.includes(' pro')) return false;
159178
if (!wantsInstant && normalizedLabel.includes('instant')) return false;
160179
if (!wantsThinking && normalizedLabel.includes('thinking')) return false;
161180
return true;
162181
};
163-
164-
if (buttonMatchesTarget()) {
182+
if (MODEL_STRATEGY === 'current') {
165183
return { status: 'already-selected', label: getButtonLabel() };
166184
}
185+
const currentSelectionMatchesTarget = () => {
186+
const labels = getSelectedModelLabels();
187+
return labels.some((label) => labelMatchesTarget(label));
188+
};
189+
190+
if (currentSelectionMatchesTarget()) {
191+
return { status: 'already-selected', label: getSelectedModelLabels()[0] || getButtonLabel() };
192+
}
167193
168194
let lastPointerClick = 0;
169195
const pointerClick = () => {
@@ -389,9 +415,9 @@ function buildModelSelectionExpression(
389415
}
390416
// Wait for the top bar label to reflect the requested model; otherwise keep scanning.
391417
setTimeout(() => {
392-
if (buttonMatchesTarget()) {
418+
if (currentSelectionMatchesTarget()) {
393419
closeMenu();
394-
resolve({ status: 'switched', label: getButtonLabel() || match.label });
420+
resolve({ status: 'switched', label: getSelectedModelLabels()[0] || getButtonLabel() || match.label });
395421
return;
396422
}
397423
attempt();

src/browser/constants.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { BrowserModelStrategy } from "./types.js";
22

33
export const CHATGPT_URL = "https://chatgpt.com/";
4-
export const DEFAULT_MODEL_TARGET = "GPT-5.4 Pro";
4+
export const DEFAULT_MODEL_TARGET = "Extended Pro";
55
export const DEFAULT_MODEL_STRATEGY: BrowserModelStrategy = "select";
66
export const COOKIE_URLS = [
77
"https://chatgpt.com",
@@ -78,7 +78,8 @@ export const SEND_BUTTON_SELECTORS = [
7878
'button[aria-label*="Send"]',
7979
];
8080
export const SEND_BUTTON_SELECTOR = SEND_BUTTON_SELECTORS[0];
81-
export const MODEL_BUTTON_SELECTOR = '[data-testid="model-switcher-dropdown-button"]';
81+
export const MODEL_BUTTON_SELECTOR =
82+
'[data-testid="model-switcher-dropdown-button"], button[aria-label="Switch model"], [aria-label="Switch model"]';
8283
export const COPY_BUTTON_SELECTOR = 'button[data-testid="copy-turn-action-button"]';
8384
// Action buttons that only appear once a turn has finished rendering.
8485
export const FINISHED_ACTIONS_SELECTOR =

src/cli/browserConfig.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ const DEFAULT_CHROME_PROFILE = "Default";
2525
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
2626
const BROWSER_MODEL_LABELS: [ModelName, string][] = [
2727
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
28-
["gpt-5.4-pro", "GPT-5.4 Pro"],
28+
["gpt-5.4-pro", "Extended Pro"],
2929
["gpt-5.2-thinking", "GPT-5.2 Thinking"],
3030
["gpt-5.2-instant", "GPT-5.2 Instant"],
31-
["gpt-5.2-pro", "GPT-5.4 Pro"],
32-
["gpt-5.1-pro", "GPT-5.4 Pro"],
33-
["gpt-5-pro", "GPT-5.4 Pro"],
31+
["gpt-5.2-pro", "Extended Pro"],
32+
["gpt-5.1-pro", "Extended Pro"],
33+
["gpt-5-pro", "Extended Pro"],
3434
// Base models last (least specific)
3535
["gpt-5.4", "Thinking 5.4"],
3636
["gpt-5.2", "GPT-5.2"], // Selects "Auto" in ChatGPT UI

tests/browser/modelSelection.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,11 @@ describe("browser model selection matchers", () => {
7070
expect(expression).toContain("key: 'Escape'");
7171
expect(expression).toContain("closeMenu();");
7272
});
73+
74+
it("checks composer pills when the top-left selector stays generic", () => {
75+
const expression = buildModelSelectionExpressionForTest("Extended Pro");
76+
expect(expression).toContain('[aria-label*="click to remove"]');
77+
expect(expression).toContain('[class*="composer-pill"]');
78+
expect(expression).toContain("currentSelectionMatchesTarget");
79+
});
7380
});

tests/cli/browserConfig.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe("buildBrowserConfig", () => {
1515
headless: undefined,
1616
keepBrowser: undefined,
1717
hideWindow: undefined,
18-
desiredModel: "GPT-5.4 Pro",
18+
desiredModel: "Extended Pro",
1919
debug: undefined,
2020
allowCookieErrors: true,
2121
});
@@ -76,7 +76,7 @@ describe("buildBrowserConfig", () => {
7676
model: "gpt-5.2-pro",
7777
browserModelLabel: "Instant",
7878
});
79-
expect(config.desiredModel).toBe("GPT-5.4 Pro");
79+
expect(config.desiredModel).toBe("Extended Pro");
8080
});
8181

8282
test("falls back to canonical label when override matches base model", async () => {
@@ -200,11 +200,11 @@ describe("buildBrowserConfig", () => {
200200

201201
describe("resolveBrowserModelLabel", () => {
202202
test("returns canonical ChatGPT label when CLI value matches API model", () => {
203-
expect(resolveBrowserModelLabel("gpt-5.4-pro", "gpt-5.4-pro")).toBe("GPT-5.4 Pro");
203+
expect(resolveBrowserModelLabel("gpt-5.4-pro", "gpt-5.4-pro")).toBe("Extended Pro");
204204
expect(resolveBrowserModelLabel("gpt-5.4", "gpt-5.4")).toBe("Thinking 5.4");
205-
expect(resolveBrowserModelLabel("gpt-5-pro", "gpt-5-pro")).toBe("GPT-5.4 Pro");
206-
expect(resolveBrowserModelLabel("gpt-5.2-pro", "gpt-5.2-pro")).toBe("GPT-5.4 Pro");
207-
expect(resolveBrowserModelLabel("gpt-5.1-pro", "gpt-5.1-pro")).toBe("GPT-5.4 Pro");
205+
expect(resolveBrowserModelLabel("gpt-5-pro", "gpt-5-pro")).toBe("Extended Pro");
206+
expect(resolveBrowserModelLabel("gpt-5.2-pro", "gpt-5.2-pro")).toBe("Extended Pro");
207+
expect(resolveBrowserModelLabel("gpt-5.1-pro", "gpt-5.1-pro")).toBe("Extended Pro");
208208
expect(resolveBrowserModelLabel("GPT-5.1", "gpt-5.1")).toBe("GPT-5.2");
209209
});
210210

@@ -217,7 +217,7 @@ describe("resolveBrowserModelLabel", () => {
217217
});
218218

219219
test("supports undefined or whitespace-only input", () => {
220-
expect(resolveBrowserModelLabel(undefined, "gpt-5.2-pro")).toBe("GPT-5.4 Pro");
220+
expect(resolveBrowserModelLabel(undefined, "gpt-5.2-pro")).toBe("Extended Pro");
221221
expect(resolveBrowserModelLabel(" ", "gpt-5.1")).toBe("GPT-5.2");
222222
});
223223

0 commit comments

Comments
 (0)