@youdie006/prodex 0.16.16 → 0.16.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,8 +32,8 @@ const EFFORT_MENU_LABELS = {
32
32
  "매우 높음": ["매우 높음", "Extra High"]
33
33
  };
34
34
  const PRO_MODE_SUBMENU_LABELS = {
35
- "기본": ["Pro 기본", "Pro Standard"],
36
- "확장": ["Pro 확장", "Pro Extended"]
35
+ "기본": ["Pro 기본", "Pro Standard", "Standard", "기본"],
36
+ "확장": ["Pro 확장", "Pro Extended", "Extended", "확장"]
37
37
  };
38
38
  const PRO_MODE_ALIASES = {
39
39
  standard: "기본",
@@ -704,6 +704,34 @@ async function dispatchMouseClickAt(cdp, x, y) {
704
704
  await cdp.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
705
705
  await cdp.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
706
706
  }
707
+ // Retry a hover-verified click whose point is transiently covered (menu/modal
708
+ // open-close animations, project navigation settling): re-locate FRESH
709
+ // coordinates each attempt. Persistent covers still fail with the refusal.
710
+ async function verifiedClickWithRetry(cdp, locate, label, deadlineMs = 5_000) {
711
+ const deadline = Date.now() + deadlineMs;
712
+ let hit = await locate();
713
+ for (;;) {
714
+ if (!hit.ok || hit.x === undefined || hit.y === undefined) {
715
+ if (Date.now() >= deadline) {
716
+ throw new Error(hit.reason ?? `${label} not found`);
717
+ }
718
+ await sleep(300);
719
+ hit = await locate();
720
+ continue;
721
+ }
722
+ try {
723
+ await verifiedClickAt(cdp, hit.x, hit.y, label);
724
+ return;
725
+ }
726
+ catch (error) {
727
+ const message = error instanceof Error ? error.message : String(error);
728
+ if (Date.now() >= deadline || !/Refusing to click/.test(message))
729
+ throw error;
730
+ await sleep(500);
731
+ hit = await locate();
732
+ }
733
+ }
734
+ }
707
735
  async function dispatchEscapeKey(cdp) {
708
736
  await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
709
737
  await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
@@ -959,6 +987,27 @@ async function selectModelReasoning(cdp, options) {
959
987
  const wantsProMode = Boolean(options.proMode) && (!options.model || /pro/i.test(options.model));
960
988
  if (wantsProMode && options.proMode) {
961
989
  // Open the Pro sub-mode submenu via the chevron, then pick 기본/확장.
990
+ // Forward-compat first: docs say Pro Standard/Extended persist, but this
991
+ // browser's picker may not render the affordance yet (staged rollout).
992
+ // Radix submenus open on ArrowRight from the focused row - keyboard is
993
+ // sturdier than a hover-revealed chevron when the flag lands.
994
+ const subLabelsForProbe = PRO_MODE_SUBMENU_LABELS[options.proMode];
995
+ const proRow = await cdp.evaluate(proRadioRectExpression());
996
+ if (proRow.ok && proRow.x !== undefined && proRow.y !== undefined) {
997
+ await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: proRow.x, y: proRow.y });
998
+ await sleep(200);
999
+ await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 });
1000
+ await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "ArrowRight", code: "ArrowRight", windowsVirtualKeyCode: 39 });
1001
+ const subViaKeyboard = await waitForExpressionTrue(cdp, menuItemPresentExpression(subLabelsForProbe), 1_500);
1002
+ if (subViaKeyboard) {
1003
+ const subHit = await cdp.evaluate(submenuItemRectExpression(subLabelsForProbe));
1004
+ if (subHit.ok && subHit.x !== undefined && subHit.y !== undefined) {
1005
+ await verifiedClickWithRetry(cdp, () => cdp.evaluate(submenuItemRectExpression(subLabelsForProbe)), subLabelsForProbe[0]);
1006
+ await assertSelectionCommitted(cdp, subLabelsForProbe[0]);
1007
+ return;
1008
+ }
1009
+ }
1010
+ }
962
1011
  const expander = await cdp.evaluate(proSubmenuExpanderRectExpression());
963
1012
  if (!expander.ok || expander.x === undefined || expander.y === undefined) {
964
1013
  // Only the expander-specific miss means "the 2026-07 update removed the
@@ -970,9 +1019,9 @@ async function selectModelReasoning(cdp, options) {
970
1019
  }
971
1020
  // The saved setup default injects pro_mode into plain asks, so the user
972
1021
  // may never have typed --pro-mode - name the clear command too.
973
- throw new Error("The ChatGPT model menu no longer offers Pro sub-modes (the 2026-07 update removed the Pro submenu; Pro is a single mode now). Use --model Pro instead of --pro-mode - and if this ask did not pass --pro-mode, a saved default is injecting it: run `prodex setup --clear-pro-mode`. If ChatGPT restores sub-modes, update prodex (npm i -g @youdie006/prodex@latest).");
1022
+ throw new Error("This ChatGPT UI does not expose Pro sub-modes yet (staged rollout: Pro Standard/Extended exist per OpenAI docs and may already show in another browser, but this picker renders Pro as a single row - keyboard and hover probes both came up empty). Use --model Pro here for now and retry --pro-mode after the UI updates. If this ask did not pass --pro-mode, a saved default is injecting it: run `prodex setup --clear-pro-mode`.");
974
1023
  }
975
- await verifiedClickAt(cdp, expander.x, expander.y, "Pro sub-mode expander");
1024
+ await verifiedClickWithRetry(cdp, () => cdp.evaluate(proSubmenuExpanderRectExpression()), "Pro sub-mode expander");
976
1025
  const subLabels = PRO_MODE_SUBMENU_LABELS[options.proMode];
977
1026
  const subVisible = await waitForExpressionTrue(cdp, menuItemPresentExpression(subLabels), MENU_OPEN_TIMEOUT_MS);
978
1027
  if (!subVisible)
@@ -981,7 +1030,7 @@ async function selectModelReasoning(cdp, options) {
981
1030
  if (!sub.ok || sub.x === undefined || sub.y === undefined) {
982
1031
  throw new Error(sub.reason ?? `ChatGPT Pro sub-mode not clickable: ${subLabels.join(" / ")}`);
983
1032
  }
984
- await verifiedClickAt(cdp, sub.x, sub.y, subLabels[0]);
1033
+ await verifiedClickWithRetry(cdp, () => cdp.evaluate(submenuItemRectExpression(subLabels)), subLabels[0]);
985
1034
  await assertSelectionCommitted(cdp, subLabels[0]);
986
1035
  return;
987
1036
  }
@@ -997,7 +1046,7 @@ async function selectModelReasoning(cdp, options) {
997
1046
  if (!proRadio.ok || proRadio.x === undefined || proRadio.y === undefined) {
998
1047
  throw new Error(proRadio.reason ?? "Pro option not found in the model menu");
999
1048
  }
1000
- await verifiedClickAt(cdp, proRadio.x, proRadio.y, "Pro");
1049
+ await verifiedClickWithRetry(cdp, () => cdp.evaluate(proRadioRectExpression()), "Pro");
1001
1050
  await assertSelectionCommitted(cdp, "Pro");
1002
1051
  return;
1003
1052
  }
@@ -1011,7 +1060,7 @@ async function selectModelReasoning(cdp, options) {
1011
1060
  if (primary.role === "menuitem" && primary.haspopup === "menu") {
1012
1061
  throw new Error(`ChatGPT model "${primaryLabel}" opens a submenu of variants instead of committing; selecting it is not supported yet. Supported today: reasoning efforts (--effort) and --model Pro.`);
1013
1062
  }
1014
- await verifiedClickAt(cdp, primary.x, primary.y, primaryLabel);
1063
+ await verifiedClickWithRetry(cdp, () => cdp.evaluate(menuItemRectExpression(primaryCandidates)), primaryLabel);
1015
1064
  await assertSelectionCommitted(cdp, primaryLabel);
1016
1065
  }
1017
1066
  catch (error) {
@@ -1141,7 +1190,7 @@ async function selectProject(cdp, options) {
1141
1190
  const detail = hit.reason && hit.reason !== "project not found in sidebar" ? ` (${hit.reason})` : "";
1142
1191
  throw new Error(`ChatGPT project not found in sidebar: ${options.project}${detail} List the visible names with \`prodex pro browser projects\`.`);
1143
1192
  }
1144
- await verifiedClickAt(cdp, hit.x, hit.y, `project ${options.project}`);
1193
+ await verifiedClickWithRetry(cdp, () => cdp.evaluate(projectItemRectExpression(options.project)), `project ${options.project}`);
1145
1194
  const navigated = await waitForExpressionTrue(cdp, `location.href !== ${JSON.stringify(hrefBefore)}`, PROJECT_NAVIGATION_TIMEOUT_MS);
1146
1195
  if (!navigated) {
1147
1196
  // The href staying put is fine ONLY when the tab was already on THIS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.16.16",
3
+ "version": "0.16.18",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",