Skip to content

feat(chatgpt): add web model switch command#1739

Open
nightwhite wants to merge 10 commits into
jackwener:mainfrom
nightwhite:feat/chatgpt-web-model
Open

feat(chatgpt): add web model switch command#1739
nightwhite wants to merge 10 commits into
jackwener:mainfrom
nightwhite:feat/chatgpt-web-model

Conversation

@nightwhite
Copy link
Copy Markdown

Summary

  • Add opencli chatgpt model <instant|thinking|pro> for ChatGPT web.
  • Reuse the existing persistent ChatGPT browser session and model picker UI.
  • Support both English and Chinese model labels for the current composer pill.
  • Register the new command in the generated CLI manifest and adapter registration test.

Validation

  • npm run build-manifest
  • npm test -- --run clis/chatgpt/commands.test.js
  • npx tsx src/main.ts validate chatgpt/model
  • Manual smoke: npx tsx src/main.ts chatgpt model instant --window foreground --site-session persistent --keep-tab true -f json
  • Manual smoke: npx tsx src/main.ts chatgpt model pro --window foreground --site-session persistent --keep-tab true -f json

Copilot AI review requested due to automatic review settings May 24, 2026 17:53
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / selectChatGPTModel helpers in clis/chatgpt/utils.js.
  • Add new CLI command module clis/chatgpt/model.js and register it in the ChatGPT command registration test.
  • Update the generated cli-manifest.json to include the new chatgpt/model command 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.

Comment thread clis/chatgpt/utils.js
Comment on lines +296 to +303
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.');

Comment thread clis/chatgpt/utils.js Outdated
Comment on lines +11 to +17
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' },
};
@nightwhite
Copy link
Copy Markdown
Author

Review updates

Addressed the two Copilot review comments:

  • Derived CHATGPT_MODEL_CHOICES from CHATGPT_MODEL_OPTIONS to avoid drift.
  • Added unit coverage for selectChatGPTModel invalid model input and missing nativeClick support, asserting the expected error types/messages.

Validation

  • npm test -- --run clis/chatgpt/utils.test.js clis/chatgpt/commands.test.js
  • npm run build-manifest
  • npx tsx src/main.ts validate chatgpt/model

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 };

Comment thread clis/chatgpt/utils.js
Comment on lines +271 to +294
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');
}
@nightwhite
Copy link
Copy Markdown
Author

Review update

Addressed the latest getCurrentChatGPTModel test coverage comment:

  • Added JSDOM-backed tests for supported visible model labels: Instant, Thinking, Pro, and 进阶专业.
  • Added coverage for the missing-selector case returning { model: null, label: null }.

Validation

  • npm test -- --run clis/chatgpt/utils.test.js clis/chatgpt/commands.test.js
  • npx tsx src/main.ts validate chatgpt/model

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

@nightwhite
Copy link
Copy Markdown
Author

Update

Added conversation metadata to chatgpt ask so long-running/research conversations can be tracked by ChatGPT's /c/<id> URL:

  • chatgpt ask now returns conversationId, conversationUrl, and response.
  • Existing chatgpt detail <id-or-url> can then poll/read that specific conversation.
  • This intentionally uses ChatGPT's own conversation id instead of inventing an OpenCLI-only task id.

Validation

  • npm run build-manifest
  • npm test -- --run clis/chatgpt/commands.test.js clis/chatgpt/utils.test.js
  • npx tsx src/main.ts validate chatgpt/ask
  • npx tsx src/main.ts validate chatgpt/detail
  • Cloud smoke: opencli chatgpt ask --new true ... -f json returned a /c/<id> URL, and opencli chatgpt detail <id> read back the same conversation.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Comment thread clis/chatgpt/ask.js Outdated
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);
Comment thread clis/chatgpt/utils.js
Comment on lines +296 to +306
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 };
@jackwener jackwener force-pushed the feat/chatgpt-web-model branch from 85a5f22 to 284ed26 Compare May 26, 2026 20:07
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.

3 participants