Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion launcher/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-web-gpt-launcher",
"version": "3.0.4",
"version": "3.0.6",
"private": true,
"description": "Desktop control center for Codex ChatGPT Web",
"author": "miuuyy",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-chatgpt-web",
"version": "3.0.4",
"version": "3.0.6",
"private": true,
"description": "A focused local Responses bridge that runs Codex tasks through a user-authenticated ChatGPT web session.",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -eu

REPOSITORY="${CODEX_CHATGPT_WEB_REPOSITORY:-miuuyy/codex-chatgpt-web}"
VERSION="${CODEX_CHATGPT_WEB_VERSION:-3.0.4}"
VERSION="${CODEX_CHATGPT_WEB_VERSION:-3.0.6}"
BIN_DIR="${CODEX_CHATGPT_WEB_BIN_DIR:-$HOME/.local/bin}"
LIB_DIR="${CODEX_CHATGPT_WEB_LIB_DIR:-$HOME/.local/lib/codex-chatgpt-web}"
DOC_DIR="${CODEX_CHATGPT_WEB_DOC_DIR:-$HOME/.local/share/doc/codex-chatgpt-web}"
Expand Down
41 changes: 35 additions & 6 deletions src/adapters/chatgpt-web/browser-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,20 @@ export class ChatGptBrowserWorker {
return await context.newPage();
}

private async activateEffortControl(control: Locator, failureMessage: string): Promise<void> {
const activated = await control.evaluate((element) => {
if (!(element instanceof HTMLButtonElement)) return false;
if (element.getAttribute("aria-haspopup") !== "menu") return false;
element.click();
return true;
});
if (activated) return;
throw new ChatGptWebAdapterError(
failureMessage,
{ status: 502, errorType: "server_error", code: "upstream_server_error", retryable: false },
);
}

private async selectModelAndEffort(
page: Page,
modelId: string,
Expand All @@ -1226,7 +1240,8 @@ export class ChatGptBrowserWorker {
await captureDiagnostic?.("luna-default-confirmed");
return mode;
}
const currentEffort = composerForm.locator(CHATGPT_EFFORT_CONTROL_SELECTOR).last();
const effortControls = composerForm.locator(CHATGPT_EFFORT_CONTROL_SELECTOR);
const currentEffort = effortControls.last();
const effortWaitAbort = new AbortController();
try {
const ready = await Promise.race([
Expand All @@ -1243,16 +1258,27 @@ export class ChatGptBrowserWorker {
}
await settleChatGptUi();
await throwIfChatGptRateLimitDialog(page);
const effortControlCount = await effortControls.filter({ visible: true }).count();
if (effortControlCount !== 1) {
throw new ChatGptWebAdapterError(
`ChatGPT exposed ${effortControlCount} visible model/effort controls; refusing ambiguous activation`,
{ status: 502, errorType: "server_error", code: "upstream_server_error", retryable: false },
);
}
await captureDiagnostic?.("effort-control-ready");
const effortMenu = page.locator(CHATGPT_EFFORT_MENU_SELECTOR).last();
const menuVisible = await effortMenu.isVisible().catch(() => false);
const menuExpanded = await currentEffort.getAttribute("aria-expanded").catch(() => null);
if (!menuVisible && menuExpanded !== "true") {
await throwIfChatGptRateLimitDialog(page);
// ChatGPT's current Radix trigger no longer responds to synthetic Enter/Space on background
// Electron surfaces. Force only the exact, visible effort control; the menu/slider state
// below remains the authoritative postcondition, so this cannot become an unproved click.
await currentEffort.click({ force: true });
// Electron can report a 1x1 viewport for a composited background WebContentsView. Playwright
// then rejects even a forced click as outside the viewport. Activate the one unambiguous
// visible button in the document and keep the opened menu/slider as the authoritative
// postcondition, so background geometry cannot turn into an unproved selection.
await this.activateEffortControl(
currentEffort,
"ChatGPT model/effort control was not an activatable menu button",
);
}
await captureDiagnostic?.("effort-menu-open-requested");
const effortChoices = effortMenu.locator(CHATGPT_EFFORT_ITEM_SELECTOR);
Expand Down Expand Up @@ -1351,7 +1377,10 @@ export class ChatGptBrowserWorker {
const expanded = await currentEffort.getAttribute("aria-expanded").catch(() => null);
if (expanded !== "true") {
await throwIfChatGptRateLimitDialog(page);
await currentEffort.click({ force: true });
await this.activateEffortControl(
currentEffort,
"ChatGPT model/effort control could not reopen its menu",
);
}
await effortChoice.waitFor({
state: "visible",
Expand Down
4 changes: 4 additions & 0 deletions src/chatgpt-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ export const CHATGPT_COMPOSER_SELECTOR = [
export const CHATGPT_EFFORT_CONTROL_SELECTOR = [
'button[aria-haspopup="menu"][data-tone="neutral"]:has([data-animated-slider-trigger="true"])',
'button[data-testid="model-switcher-dropdown-button"][aria-haspopup="menu"]',
// Current ChatGPT builds expose the model/effort trigger as a neutral composer pill without
// either of the older identifying descendants. Selection code requires one unambiguous visible
// match and verifies the opened effort menu before it can submit anything.
'button[aria-haspopup="menu"][data-tone="neutral"]',
].join(", ");
export const CHATGPT_EFFORT_MENU_SELECTOR = [
'[data-testid="composer-intelligence-picker-content"]:has([role="menuitemradio"], [data-model-reasoning-effort-slider])',
Expand Down
2 changes: 1 addition & 1 deletion src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const VERSION = "3.0.4";
export const VERSION = "3.0.6";
10 changes: 8 additions & 2 deletions tests/browser-worker-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -997,14 +997,18 @@ test("effort selection handles the known ChatGPT rate-limit dialog before backgr
const selectionEnd = workerSource.indexOf("private async activeComposer", selectionStart);
const selectionSource = workerSource.slice(selectionStart, selectionEnd);
const guard = selectionSource.indexOf("throwIfChatGptRateLimitDialog(page)");
const activation = selectionSource.indexOf("currentEffort.click({ force: true })");
const activation = selectionSource.indexOf("this.activateEffortControl(");

expect(workerSource).toContain("Too many requests");
expect(workerSource).toContain("making requests too quickly");
expect(guard).toBeGreaterThan(-1);
expect(activation).toBeGreaterThan(guard);
expect(selectionSource).not.toContain('currentEffort.press("Enter")');
expect(selectionSource).not.toContain("currentEffort.evaluate(");
expect(selectionSource).not.toContain("currentEffort.click(");
expect(selectionSource).toContain("effortControlCount !== 1");
expect(workerSource).toContain("element instanceof HTMLButtonElement");
expect(workerSource).toContain('element.getAttribute("aria-haspopup") !== "menu"');
expect(workerSource).toContain("element.click()");
expect(selectionSource).toContain('effortChoice.press("Enter")');
expect(selectionSource).not.toContain("effortChoice.click(");
expect(selectionSource).not.toContain("is unavailable");
Expand Down Expand Up @@ -1257,7 +1261,9 @@ test("effort selection stops as soon as ChatGPT reports an expired session", asy
test("effort menu waiting stops when ChatGPT reports an expired session", async () => {
const neverVisible = new Promise<void>(() => {});
const effortControl = {
filter() { return this; },
last() { return this; },
count: async () => 1,
waitFor: async () => {},
getAttribute: async () => "true",
};
Expand Down
2 changes: 1 addition & 1 deletion tests/chatgpt-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ test("the effort selector identifies the model slider instead of any composer me
expect(selectors).toContain(
'button[data-testid="model-switcher-dropdown-button"][aria-haspopup="menu"]',
);
expect(selectors).not.toContain('button[aria-haspopup="menu"][data-tone="neutral"]');
expect(selectors).toContain('button[aria-haspopup="menu"][data-tone="neutral"]');
expect(selectors).not.toContain('button[aria-haspopup="menu"]');
});

Expand Down