feat(chatgpt): add web model switch command#1739
Conversation
There was a problem hiding this comment.
Pull request overview
Adds a new chatgpt model browser command to switch the active ChatGPT web model (instant/thinking/pro) using the existing persistent site session and the composer’s model picker UI.
Changes:
- Introduce model constants plus
getCurrentChatGPTModel/selectChatGPTModelhelpers inclis/chatgpt/utils.js. - Add new CLI command module
clis/chatgpt/model.jsand register it in the ChatGPT command registration test. - Update the generated
cli-manifest.jsonto include the newchatgpt/modelcommand entry.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| clis/chatgpt/utils.js | Adds model choice metadata and browser-automation helpers to detect/switch the current ChatGPT web model. |
| clis/chatgpt/model.js | New CLI command wiring for opencli chatgpt model <instant|thinking|pro>. |
| clis/chatgpt/commands.test.js | Ensures the new command is registered and exposes the expected args/columns. |
| cli-manifest.json | Registers the new command in the generated manifest output. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export async function selectChatGPTModel(page, model) { | ||
| const target = requireKnownChatGPTModel(model); | ||
| if (typeof page.nativeClick !== 'function') { | ||
| throw new CommandExecutionError('ChatGPT model selection requires native browser click support.'); | ||
| } | ||
| await ensureOnChatGPT(page); | ||
| await ensureChatGPTComposer(page, 'ChatGPT model selection requires a logged-in ChatGPT session with a visible composer.'); | ||
|
|
| export const CHATGPT_MODEL_CHOICES = ['instant', 'thinking', 'pro']; | ||
|
|
||
| const CHATGPT_MODEL_OPTIONS = { | ||
| instant: { label: 'Instant', labels: ['Instant', '即时'], testId: 'model-switcher-gpt-5-5' }, | ||
| thinking: { label: 'Thinking', labels: ['Thinking', '思考'], testId: 'model-switcher-gpt-5-5-thinking' }, | ||
| pro: { label: 'Pro', labels: ['Pro', '进阶专业'], testId: 'model-switcher-gpt-5-5-pro' }, | ||
| }; |
Review updatesAddressed the two Copilot review comments:
Validation
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Comments suppressed due to low confidence (1)
clis/chatgpt/utils.js:370
- selectChatGPTModel introduces a multi-step UI interaction (locate model pill, open menu via nativeClick, find option by data-testid with retries, then verify the switch). There are only validation tests right now; consider adding a unit test that exercises the happy-path script(s) against a JSDOM page mock to catch selector/testId regressions and ensure the retry loop behaves as expected.
export async function selectChatGPTModel(page, model) {
const target = requireKnownChatGPTModel(model);
if (typeof page.nativeClick !== 'function') {
throw new CommandExecutionError('ChatGPT model selection requires native browser click support.');
}
await ensureOnChatGPT(page);
await ensureChatGPTComposer(page, 'ChatGPT model selection requires a logged-in ChatGPT session with a visible composer.');
const before = await getCurrentChatGPTModel(page);
if (before.model === target.key) {
return { Status: 'Already selected', Model: target.label };
}
const menuButton = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => {
const isVisible = (el) => {
if (!(el instanceof HTMLElement)) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
const labels = ${JSON.stringify(Object.values(CHATGPT_MODEL_OPTIONS).flatMap((entry) => entry.labels))};
const button = Array.from(document.querySelectorAll('form button')).find((node) =>
isVisible(node) && labels.includes(normalize(node.textContent))
);
if (!button) return { found: false };
button.scrollIntoView({ block: 'center', inline: 'center' });
const rect = button.getBoundingClientRect();
return {
found: true,
x: Math.round(rect.left + rect.width / 2),
y: Math.round(rect.top + rect.height / 2),
};
})()`)), 'chatgpt model menu button');
if (!menuButton.found) {
throw new CommandExecutionError('Could not find the ChatGPT model selector in the composer.');
}
await page.nativeClick(Number(menuButton.x), Number(menuButton.y));
await page.wait(0.5);
let optionCenter = null;
for (let attempt = 0; attempt < 10; attempt += 1) {
optionCenter = requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => {
const isVisible = (el) => {
if (!(el instanceof HTMLElement)) return false;
const style = window.getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
};
const option = document.querySelector(${JSON.stringify(`[data-testid="${target.testId}"]`)});
if (!(option instanceof HTMLElement) || !isVisible(option)) return { found: false };
option.scrollIntoView({ block: 'center', inline: 'center' });
const rect = option.getBoundingClientRect();
return {
found: true,
x: Math.round(rect.left + rect.width / 2),
y: Math.round(rect.top + rect.height / 2),
};
})()`)), 'chatgpt model option click');
if (optionCenter.found) break;
await page.wait(0.5);
}
if (!optionCenter?.found) {
throw new CommandExecutionError(`Could not click the ChatGPT ${target.label} model option.`);
}
await page.nativeClick(Number(optionCenter.x), Number(optionCenter.y));
await page.wait(0.5);
const after = await getCurrentChatGPTModel(page);
if (after.model !== target.key) {
throw new CommandExecutionError(`ChatGPT model did not switch to ${target.label}.`);
}
return { Status: 'Success', Model: target.label };
| export async function getCurrentChatGPTModel(page) { | ||
| return requireObjectEvaluateResult(unwrapEvaluateResult(await page.evaluate(`(() => { | ||
| const isVisible = (el) => { | ||
| if (!(el instanceof HTMLElement)) return false; | ||
| const style = window.getComputedStyle(el); | ||
| if (style.display === 'none' || style.visibility === 'hidden') return false; | ||
| const rect = el.getBoundingClientRect(); | ||
| return rect.width > 0 && rect.height > 0; | ||
| }; | ||
| const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim(); | ||
| const labels = ${JSON.stringify(CHATGPT_MODEL_OPTIONS)}; | ||
| const button = Array.from(document.querySelectorAll('form button')).find((node) => { | ||
| if (!isVisible(node)) return false; | ||
| const text = normalize(node.textContent); | ||
| return Object.values(labels).some((entry) => entry.labels.includes(text)); | ||
| }); | ||
| const label = normalize(button?.textContent || ''); | ||
| const entry = Object.entries(labels).find(([, value]) => value.labels.includes(label)); | ||
| return { | ||
| model: entry?.[0] ?? null, | ||
| label: entry?.[1]?.label ?? null, | ||
| }; | ||
| })()`)), 'chatgpt current model'); | ||
| } |
Review updateAddressed the latest
Validation
|
UpdateAdded conversation metadata to
Validation
|
| return [{ response: await waitForChatGPTResponse(page, baseline, prompt, timeout) }]; | ||
| const response = await waitForChatGPTResponse(page, baseline, prompt, timeout); | ||
| const conversationUrl = await currentChatGPTUrl(page); | ||
| const conversationId = parseChatGPTConversationId(conversationUrl); |
| export async function selectChatGPTModel(page, model) { | ||
| const target = requireKnownChatGPTModel(model); | ||
| if (typeof page.nativeClick !== 'function') { | ||
| throw new CommandExecutionError('ChatGPT model selection requires native browser click support.'); | ||
| } | ||
| await ensureOnChatGPT(page); | ||
| await ensureChatGPTComposer(page, 'ChatGPT model selection requires a logged-in ChatGPT session with a visible composer.'); | ||
|
|
||
| const before = await getCurrentChatGPTModel(page); | ||
| if (before.model === target.key) { | ||
| return { Status: 'Already selected', Model: target.label }; |
85a5f22 to
284ed26
Compare
Summary
opencli chatgpt model <instant|thinking|pro>for ChatGPT web.Validation
npm run build-manifestnpm test -- --run clis/chatgpt/commands.test.jsnpx tsx src/main.ts validate chatgpt/modelnpx tsx src/main.ts chatgpt model instant --window foreground --site-session persistent --keep-tab true -f jsonnpx tsx src/main.ts chatgpt model pro --window foreground --site-session persistent --keep-tab true -f json