@youdie006/prodex 0.36.4 → 0.36.5

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.
@@ -5,6 +5,7 @@ import net from "node:net";
5
5
  import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import os from "node:os";
8
+ import { readPowerSliderSelection } from "./picker-interaction.js";
8
9
  export class ChatGptBrowserBlockerError extends Error {
9
10
  blocker;
10
11
  constructor(blocker) {
@@ -1300,6 +1301,14 @@ const CLICK_POINT_SNIPPET = `
1300
1301
  el.scrollIntoView({ block: "center", inline: "nearest" });
1301
1302
  const r = el.getBoundingClientRect();
1302
1303
  if (r.width < 1 || r.height < 1) return { ok: false, reason: "target element has no visible area" };
1304
+ // An inert row cannot be clicked at ANY point, so say that instead of
1305
+ // blaming whatever hit-testing returns instead. Measured on the current
1306
+ // picker: the model rows carry pointer-events:none and sit outside the
1307
+ // menu's own box, so every coordinate on them resolves to the effort
1308
+ // control and the refusal read as "another element covers it".
1309
+ if (getComputedStyle(el).pointerEvents === "none") {
1310
+ return { ok: false, reason: "target element is not clickable (pointer-events: none)" };
1311
+ }
1303
1312
  el.setAttribute('data-prodex-click', '1');
1304
1313
  const x = Math.round(r.x + r.width / 2);
1305
1314
  const y = Math.round(r.y + (yCap ? Math.min(r.height / 2, yCap) : r.height / 2));
@@ -1382,15 +1391,24 @@ export function powerSliderStateExpression() {
1382
1391
  const slider = document.querySelector('[role="slider"]');
1383
1392
  const menu = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
1384
1393
  const lines = menu ? (menu.innerText || "").split(String.fromCharCode(10)).map((l) => l.trim()).filter(Boolean) : [];
1385
- const after = (label) => { const i = lines.indexOf(label); return i >= 0 ? lines[i + 1] : null; };
1386
1394
  if (!slider) return { ok: false, reason: "power slider not found", lines };
1395
+ const readPowerSliderSelection = ${readPowerSliderSelection.toString()};
1396
+ const selection = readPowerSliderSelection({
1397
+ sliderValueText: slider.getAttribute("aria-valuetext"),
1398
+ items: menu ? [...menu.querySelectorAll('[role="menuitemradio"],[role="menuitem"]')].map((item) => ({
1399
+ role: item.getAttribute("role"),
1400
+ text: item.innerText || item.textContent || "",
1401
+ checked: item.getAttribute("aria-checked") === "true",
1402
+ containsSlider: item.contains(slider)
1403
+ })) : []
1404
+ });
1387
1405
  return {
1388
1406
  ok: true,
1389
1407
  position: Number(slider.getAttribute("aria-valuenow")),
1390
1408
  min: Number(slider.getAttribute("aria-valuemin")),
1391
1409
  max: Number(slider.getAttribute("aria-valuemax")),
1392
- model: after("Model"),
1393
- effort: after("Effort"),
1410
+ model: selection.model,
1411
+ effort: selection.effort,
1394
1412
  lines
1395
1413
  };
1396
1414
  })()`;
@@ -1562,11 +1580,32 @@ const POWER_SLIDER_APPEAR_ATTEMPTS = 5;
1562
1580
  // Click a model row. They are ordinary `menuitemradio` entries in the same
1563
1581
  // menu as the effort slider, so the existing by-label lookup reaches them; what
1564
1582
  // was missing is anything routing a model here instead of into the slider.
1565
- async function selectPickerModel(cdp, requested) {
1583
+ /**
1584
+ * Whether a model selection failure is the picker not offering it at all.
1585
+ *
1586
+ * Those two cases - the row is absent, or it is inert - are the UI declining to
1587
+ * provide the choice, and they must not kill a send that is otherwise fine. It
1588
+ * is the same answer prodex already gives for a Pro sub-mode it cannot apply.
1589
+ * Anything else (a menu that would not open, a click that would not land) is a
1590
+ * real failure and stays one, so a genuine break is not buried under a warning.
1591
+ */
1592
+ export function modelSelectionUnavailableWarning(requested, reason, available) {
1593
+ if (!/menu item not found|not clickable/i.test(reason))
1594
+ return undefined;
1595
+ const offers = available?.length ? ` It offers: ${available.join(", ")}.` : "";
1596
+ return (`model_not_applied: this ChatGPT picker does not offer "${requested}" as a selectable model, ` +
1597
+ `so the send used whatever the composer already had.${offers} ` +
1598
+ 'Pick an effort with --effort instead, or clear a saved default with `prodex setup --model ""`.');
1599
+ }
1600
+ async function selectPickerModel(cdp, requested, warnings = []) {
1566
1601
  const hit = await cdp.evaluate(menuItemRectExpression(requested));
1567
1602
  if (!hit.ok || hit.x === undefined || hit.y === undefined) {
1568
- const available = hit.available?.length ? ` The picker offers: ${hit.available.join(", ")}.` : "";
1569
- throw new Error(`ChatGPT's model picker has no "${requested}" model.${available}`);
1603
+ const unavailable = modelSelectionUnavailableWarning(requested, hit.reason ?? "", hit.available);
1604
+ if (unavailable) {
1605
+ warnings.push(unavailable);
1606
+ return;
1607
+ }
1608
+ throw new Error(hit.reason ?? `ChatGPT's model picker has no "${requested}" model.`);
1570
1609
  }
1571
1610
  await verifiedClickWithRetry(cdp, () => cdp.evaluate(menuItemRectExpression(requested)), requested);
1572
1611
  }
@@ -1689,7 +1728,7 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
1689
1728
  await selectPowerStep(cdp, plan.sliderLabel);
1690
1729
  }
1691
1730
  if (plan.modelLabel) {
1692
- await selectPickerModel(cdp, plan.modelLabel);
1731
+ await selectPickerModel(cdp, plan.modelLabel, selectionWarnings);
1693
1732
  }
1694
1733
  // This branch cannot honour a sub-mode, and used to return without
1695
1734
  // saying so - the caller got a clean receipt for a 확장 it never got.
@@ -0,0 +1,40 @@
1
+ /** Pure decision helpers for ChatGPT's composer picker, kept out of the page. */
2
+ /** Read model and effort from both the current mixed-control picker and its prior labeled-row form. */
3
+ export function readPowerSliderSelection(snapshot) {
4
+ const items = snapshot.items.map((item) => ({
5
+ ...item,
6
+ lines: item.text
7
+ .split("\n")
8
+ .map((line) => line.trim())
9
+ .filter(Boolean)
10
+ }));
11
+ const powerLabels = new Set([
12
+ "instant",
13
+ "즉시",
14
+ "빠름",
15
+ "fast",
16
+ "medium",
17
+ "중간",
18
+ "보통",
19
+ "high",
20
+ "높음",
21
+ "extra high",
22
+ "extrahigh",
23
+ "very high",
24
+ "매우 높음",
25
+ "매우높음",
26
+ "pro",
27
+ "프로"
28
+ ]);
29
+ const legacyModel = items.find((item) => item.lines[0] === "Model")?.lines[1] ?? null;
30
+ const legacyEffort = items.find((item) => item.lines[0] === "Effort")?.lines[1] ?? null;
31
+ const checkedModel = items.find((item) => item.role === "menuitemradio" && item.checked)?.lines[0] ?? null;
32
+ const sliderOwner = items.find((item) => item.role === "menuitem" && item.containsSlider)?.lines[0] ?? null;
33
+ const visiblePowerLabel = items.find((item) => item.role === "menuitem" && powerLabels.has((item.lines[0] ?? "").toLowerCase()))?.lines[0] ?? null;
34
+ const accessibleCandidate = snapshot.sliderValueText?.trim() || null;
35
+ const accessibleValue = accessibleCandidate && powerLabels.has(accessibleCandidate.toLowerCase()) ? accessibleCandidate : null;
36
+ return {
37
+ model: legacyModel ?? checkedModel,
38
+ effort: accessibleValue ?? legacyEffort ?? sliderOwner ?? visiblePowerLabel
39
+ };
40
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.36.4",
3
+ "version": "0.36.5",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",