@youdie006/prodex 0.39.0 → 0.39.1

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.
@@ -6,7 +6,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
6
6
  import path from "node:path";
7
7
  import { captureBrowserDiagnostics, diagnosticsEnabled, diagnosticsNote } from "./browser-diagnostics.js";
8
8
  import os from "node:os";
9
- import { menuKeyboardStep, readPowerSliderSelection } from "./picker-interaction.js";
9
+ import { chatSurfaceChoice, menuKeyboardStep, readPowerSliderSelection, sliderRestoreStep } from "./picker-interaction.js";
10
10
  export class ChatGptBrowserBlockerError extends Error {
11
11
  blocker;
12
12
  constructor(blocker) {
@@ -22,6 +22,8 @@ export class ChatGptBrowserBlockerError extends Error {
22
22
  // answered with advice to "say --effort Pro", which then failed.
23
23
  /** A full trip round the menu is far more than any real picker needs. */
24
24
  const MENU_KEYBOARD_WALK_LIMIT = 16;
25
+ /** Enough presses to cross any slider this code accepts, twice over. */
26
+ const POWER_SLIDER_RESTORE_ATTEMPTS = 26;
25
27
  // Two more rungs than before. Measured live: with the model row chosen rather
26
28
  // than the recommended set, the slider reads Light / Medium / High / Extra High
27
29
  // / Max / Ultra, and Ultra is the top of the machine. Pro is kept because older
@@ -1445,6 +1447,20 @@ export function activeMenuItemExpression() {
1445
1447
  return { role: a.getAttribute("role"), label };
1446
1448
  })()`;
1447
1449
  }
1450
+ export function chatWorkSurfaceExpression() {
1451
+ return `(() => {${CLICK_POINT_SNIPPET}
1452
+ const buttons = [...document.querySelectorAll('[role="radio"],[role="tab"]')].filter((el) => {
1453
+ const t = ((el.innerText || el.textContent || "").trim());
1454
+ return t === "Chat" || t === "Work";
1455
+ });
1456
+ const surfaces = buttons.map((el) => ({
1457
+ label: ((el.innerText || el.textContent || "").trim()),
1458
+ checked: el.getAttribute("aria-checked") === "true" || el.getAttribute("aria-selected") === "true"
1459
+ }));
1460
+ const chat = buttons.find((el) => ((el.innerText || el.textContent || "").trim()) === "Chat");
1461
+ return { surfaces, chat: chat ? clickPoint(chat) : { ok: false, reason: "no Chat toggle" } };
1462
+ })()`;
1463
+ }
1448
1464
  export function powerSliderPresentExpression() {
1449
1465
  return `Boolean(document.querySelector('[data-testid="composer-intelligence-picker-content"] [role="slider"]'))`;
1450
1466
  }
@@ -1782,6 +1798,33 @@ async function selectPickerModel(cdp, requested, warnings = []) {
1782
1798
  * without this the step that follows reported "the picker did not expose its
1783
1799
  * power slider" on a picker that was simply shut.
1784
1800
  */
1801
+ /**
1802
+ * Put the browser back on ChatGPT's Chat surface when it has drifted onto Work.
1803
+ *
1804
+ * The two have different pickers - Work's offers no Pro at all - and nothing on
1805
+ * the page announces which one is live, so a drifted browser silently drives
1806
+ * the wrong picker. Returns a warning to carry to the caller when it moved.
1807
+ */
1808
+ async function ensureChatSurface(cdp) {
1809
+ let probe;
1810
+ try {
1811
+ probe = await cdp.evaluate(chatWorkSurfaceExpression());
1812
+ }
1813
+ catch {
1814
+ return undefined;
1815
+ }
1816
+ if (!probe)
1817
+ return undefined;
1818
+ if (chatSurfaceChoice(probe.surfaces ?? []) !== "switch-to-chat")
1819
+ return undefined;
1820
+ const chat = probe.chat;
1821
+ if (!chat?.ok || chat.x === undefined || chat.y === undefined) {
1822
+ return "ChatGPT is on its Work surface, whose picker offers different models and no Pro, and the Chat toggle could not be clicked.";
1823
+ }
1824
+ await verifiedClickAt(cdp, chat.x, chat.y, "Chat surface toggle");
1825
+ await sleep(1_500);
1826
+ return "ChatGPT was on its Work surface, whose picker offers different models and no Pro. Switched back to Chat.";
1827
+ }
1785
1828
  async function ensurePickerOpen(cdp) {
1786
1829
  // The slider, not the menu, is what the caller needs - and an open menu that
1787
1830
  // has not painted its slider yet looks identical to one that has.
@@ -1870,6 +1913,11 @@ export function sliderWalkPlan(state) {
1870
1913
  async function selectModelReasoning(cdp, options, selectionWarnings = []) {
1871
1914
  if (!options.model && !options.proMode && !options.effort)
1872
1915
  return;
1916
+ // Which surface is live decides which picker exists at all, so settle that
1917
+ // before looking for anything in it.
1918
+ const surfaceWarning = await ensureChatSurface(cdp);
1919
+ if (surfaceWarning)
1920
+ selectionWarnings.push(surfaceWarning);
1873
1921
  // --pro-mode selects a Pro sub-mode, so it is meaningless with a non-Pro
1874
1922
  // --model. Fail loudly instead of silently dropping the requested sub-mode.
1875
1923
  if (options.proMode && options.model && !/pro/i.test(options.model)) {
@@ -3380,25 +3428,25 @@ export async function listChatGptSidebarProjects(input = {}) {
3380
3428
  * listing exactly as it was rather than failing a read-only command.
3381
3429
  */
3382
3430
  async function readEffortSteps(cdp) {
3431
+ const focused = await cdp.evaluate(focusPowerSliderExpression());
3432
+ if (!focused?.ok)
3433
+ return undefined;
3434
+ const start = await cdp.evaluate(powerSliderStateExpression());
3435
+ if (!start?.ok)
3436
+ return undefined;
3437
+ const plan = sliderWalkPlan({ position: start.position, min: start.min, max: start.max });
3438
+ if (!plan)
3439
+ return undefined;
3440
+ const current = start.effort ?? null;
3441
+ const startedAt = start.position;
3383
3442
  try {
3384
- const focused = await cdp.evaluate(focusPowerSliderExpression());
3385
- if (!focused?.ok)
3386
- return undefined;
3387
- const start = await cdp.evaluate(powerSliderStateExpression());
3388
- if (!start?.ok)
3389
- return undefined;
3390
- const plan = sliderWalkPlan({ position: start.position, min: start.min, max: start.max });
3391
- if (!plan)
3392
- return undefined;
3393
- const current = start.effort ?? null;
3394
3443
  for (let i = 0; i < plan.toBottom; i += 1) {
3395
3444
  await dispatchArrowKey(cdp, "ArrowLeft");
3396
3445
  await sleep(120);
3397
3446
  }
3398
- // Every position is recorded, including repeats. One slider now walks a
3399
- // ladder of model-and-effort pairs, so "Light" is three different rungs -
3400
- // collapsing by label turned six positions into three and lost which model
3401
- // each one sends with.
3447
+ // Every position is recorded, including repeats. The slider can walk a
3448
+ // ladder of model-and-effort pairs, so one effort name is several rungs -
3449
+ // collapsing by label lost which model each one would send with.
3402
3450
  const rungs = [];
3403
3451
  const record = async () => {
3404
3452
  const state = await cdp.evaluate(powerSliderStateExpression());
@@ -3415,16 +3463,40 @@ async function readEffortSteps(cdp) {
3415
3463
  await sleep(220);
3416
3464
  await record();
3417
3465
  }
3418
- // Put it back. Reading someone's setting must not change it.
3419
- for (let i = 0; i < plan.climb - plan.back; i += 1) {
3420
- await dispatchArrowKey(cdp, "ArrowLeft");
3421
- await sleep(120);
3422
- }
3423
3466
  return rungs.length > 0 ? { rungs, current } : undefined;
3424
3467
  }
3425
3468
  catch {
3426
3469
  return undefined;
3427
3470
  }
3471
+ finally {
3472
+ // Whatever happened above, the setting goes back. Replaying a precomputed
3473
+ // count skipped this whenever the walk was interrupted, and one run left a
3474
+ // slider on High that had been on Pro.
3475
+ await restorePowerSlider(cdp, startedAt);
3476
+ }
3477
+ }
3478
+ async function restorePowerSlider(cdp, target) {
3479
+ for (let guard = 0; guard < POWER_SLIDER_RESTORE_ATTEMPTS; guard += 1) {
3480
+ let state;
3481
+ try {
3482
+ state = await cdp.evaluate(powerSliderStateExpression());
3483
+ }
3484
+ catch {
3485
+ return;
3486
+ }
3487
+ if (!state?.ok)
3488
+ return;
3489
+ const move = sliderRestoreStep({ ...(state.position !== undefined ? { current: state.position } : {}), ...(target !== undefined ? { target } : {}) });
3490
+ if (move === "done")
3491
+ return;
3492
+ try {
3493
+ await dispatchArrowKey(cdp, move === "left" ? "ArrowLeft" : "ArrowRight");
3494
+ }
3495
+ catch {
3496
+ return;
3497
+ }
3498
+ await sleep(150);
3499
+ }
3428
3500
  }
3429
3501
  export async function listChatGptModelOptions(input = {}) {
3430
3502
  const port = resolveCdpPort(input.port);
@@ -3460,7 +3532,18 @@ export async function listChatGptModelOptions(input = {}) {
3460
3532
  throw new Error("ChatGPT model menu did not open after clicking the selector");
3461
3533
  const options = await cdp.evaluate(modelMenuOptionsExpression());
3462
3534
  const effortSteps = await readEffortSteps(cdp);
3463
- return { url: status.url, options, ...(effortSteps ? { effortSteps } : {}) };
3535
+ // Chat and Work have different pickers - Work's offers no Pro - and a
3536
+ // listing that does not say which one it read is how a browser sitting on
3537
+ // Work convinced two machines that Pro had been removed from the account.
3538
+ let surface;
3539
+ try {
3540
+ const probe = await cdp.evaluate(chatWorkSurfaceExpression());
3541
+ surface = probe?.surfaces?.find((entry) => entry.checked)?.label;
3542
+ }
3543
+ catch {
3544
+ // the toggle predates some builds; saying nothing is right there
3545
+ }
3546
+ return { url: status.url, options, ...(effortSteps ? { effortSteps } : {}), ...(surface ? { surface } : {}) };
3464
3547
  }
3465
3548
  finally {
3466
3549
  try {
package/dist/cli-pro.js CHANGED
@@ -443,6 +443,9 @@ export async function runProCommand(rest, io, runCliFn) {
443
443
  });
444
444
  throw new Error(blocker.next_step ? `${blocker.message} Next: ${blocker.next_step}` : errorMessage(error));
445
445
  }
446
+ if (listed.surface) {
447
+ io.stdout(`Read from ChatGPT's ${listed.surface} surface. Chat and Work have different pickers - only one of them offers Pro.`);
448
+ }
446
449
  io.stdout("Model menu options in the visible ChatGPT tab (read-only; nothing was selected):");
447
450
  for (const option of listed.options) {
448
451
  io.stdout(formatModelMenuOption(option));
@@ -70,3 +70,30 @@ export function menuKeyboardStep(input) {
70
70
  }
71
71
  return pressed < limit ? "down" : "exhausted";
72
72
  }
73
+ /**
74
+ * One move towards putting the power slider back where the walk found it.
75
+ *
76
+ * Driven by the position actually read rather than by a replayed count, so an
77
+ * interrupted walk still converges instead of abandoning the setting wherever
78
+ * it stopped.
79
+ */
80
+ export function sliderRestoreStep(input) {
81
+ const { current, target } = input;
82
+ if (!Number.isInteger(current) || !Number.isInteger(target))
83
+ return "done";
84
+ if (current === target)
85
+ return "done";
86
+ return current > target ? "left" : "right";
87
+ }
88
+ /**
89
+ * Decide whether the browser needs putting back on ChatGPT's Chat surface.
90
+ *
91
+ * Chat and Work have different model pickers - Work's has no Pro at all - so a
92
+ * browser that has drifted onto Work silently drives the wrong one.
93
+ */
94
+ export function chatSurfaceChoice(surfaces) {
95
+ const chat = surfaces.find((surface) => surface.label.trim().toLowerCase() === "chat");
96
+ if (!chat)
97
+ return "no-toggle";
98
+ return chat.checked ? "already-chat" : "switch-to-chat";
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youdie006/prodex",
3
- "version": "0.39.0",
3
+ "version": "0.39.1",
4
4
  "description": "Local receipt bus for coordinating Codex execution with ChatGPT Pro/Projects consultation.",
5
5
  "author": "youdie006",
6
6
  "license": "MIT",