@youdie006/prodex 0.38.1 → 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.
- package/dist/chatgpt-browser.js +243 -33
- package/dist/cli-pro.js +14 -4
- package/dist/config.js +1 -1
- package/dist/picker-interaction.js +61 -2
- package/package.json +1 -1
package/dist/chatgpt-browser.js
CHANGED
|
@@ -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 { 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) {
|
|
@@ -20,7 +20,15 @@ export class ChatGptBrowserBlockerError extends Error {
|
|
|
20
20
|
// of this control rather than a model. Leaving it out meant --effort could not
|
|
21
21
|
// reach the top of the slider at all, and the only route there - --model Pro -
|
|
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
|
+
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;
|
|
27
|
+
// Two more rungs than before. Measured live: with the model row chosen rather
|
|
28
|
+
// than the recommended set, the slider reads Light / Medium / High / Extra High
|
|
29
|
+
// / Max / Ultra, and Ultra is the top of the machine. Pro is kept because older
|
|
30
|
+
// pickers still name the top step that way.
|
|
31
|
+
const REASONING_EFFORTS = ["즉시", "중간", "높음", "매우 높음", "Max", "Ultra", "Pro"];
|
|
24
32
|
const PRO_MODES = ["기본", "확장"];
|
|
25
33
|
// Aliases map friendly CLI input onto the exact Korean menu labels the picker
|
|
26
34
|
// clicks by text. Keys are lowercased and space-stripped before lookup.
|
|
@@ -29,19 +37,26 @@ const REASONING_EFFORT_ALIASES = {
|
|
|
29
37
|
instant: "즉시",
|
|
30
38
|
medium: "중간",
|
|
31
39
|
high: "높음",
|
|
32
|
-
max: "매우 높음",
|
|
33
40
|
extrahigh: "매우 높음",
|
|
41
|
+
max: "매우 높음",
|
|
42
|
+
light: "즉시",
|
|
43
|
+
"가벼움": "즉시",
|
|
44
|
+
"최대": "Max",
|
|
45
|
+
ultra: "Ultra",
|
|
46
|
+
"울트라": "Ultra",
|
|
34
47
|
pro: "Pro",
|
|
35
48
|
"프로": "Pro"
|
|
36
49
|
};
|
|
37
50
|
// Menu labels per canonical value, verified live in both the Korean and the
|
|
38
51
|
// English (US) ChatGPT UI. Matching tries every candidate so either UI works.
|
|
39
52
|
const EFFORT_MENU_LABELS = {
|
|
40
|
-
"즉시": ["즉시", "Instant"],
|
|
53
|
+
"즉시": ["즉시", "Instant", "Light"],
|
|
41
54
|
"중간": ["중간", "Medium"],
|
|
42
55
|
"높음": ["높음", "High"],
|
|
43
56
|
"매우 높음": ["매우 높음", "Extra High"],
|
|
44
|
-
|
|
57
|
+
Max: ["Max", "최대"],
|
|
58
|
+
Ultra: ["Ultra", "울트라"],
|
|
59
|
+
Pro: ["Pro", "프로", "6-pro", "GPT-6 Pro", "6 Pro"]
|
|
45
60
|
};
|
|
46
61
|
const PRO_MODE_SUBMENU_LABELS = {
|
|
47
62
|
"기본": ["Pro 기본", "Pro Standard", "Standard", "기본"],
|
|
@@ -60,7 +75,7 @@ export function parseReasoningEffort(raw) {
|
|
|
60
75
|
const alias = REASONING_EFFORT_ALIASES[trimmed.toLowerCase().replace(/\s+/g, "")];
|
|
61
76
|
if (alias)
|
|
62
77
|
return alias;
|
|
63
|
-
throw new Error(`--effort must be one of ${REASONING_EFFORTS.join(", ")} (English aliases: instant, medium, high, max)`);
|
|
78
|
+
throw new Error(`--effort must be one of ${REASONING_EFFORTS.join(", ")} (English aliases: instant/light, medium, high, extrahigh, max, ultra)`);
|
|
64
79
|
}
|
|
65
80
|
/** Normalize a CLI Pro sub-mode value onto the exact ChatGPT menu label. */
|
|
66
81
|
export function parseProMode(raw) {
|
|
@@ -1422,6 +1437,52 @@ export function powerSliderStateExpression() {
|
|
|
1422
1437
|
};
|
|
1423
1438
|
})()`;
|
|
1424
1439
|
}
|
|
1440
|
+
export function activeMenuItemExpression() {
|
|
1441
|
+
return `(() => {
|
|
1442
|
+
const a = document.activeElement;
|
|
1443
|
+
if (!a) return null;
|
|
1444
|
+
const menu = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
|
|
1445
|
+
if (menu && !menu.contains(a)) return null;
|
|
1446
|
+
const label = ((a.innerText || a.textContent || "").trim().split(String.fromCharCode(10))[0] || "").trim();
|
|
1447
|
+
return { role: a.getAttribute("role"), label };
|
|
1448
|
+
})()`;
|
|
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
|
+
}
|
|
1464
|
+
export function powerSliderPresentExpression() {
|
|
1465
|
+
return `Boolean(document.querySelector('[data-testid="composer-intelligence-picker-content"] [role="slider"]'))`;
|
|
1466
|
+
}
|
|
1467
|
+
export function pickerClosedExpression() {
|
|
1468
|
+
return `!document.querySelector('[data-testid="composer-intelligence-picker-content"]')`;
|
|
1469
|
+
}
|
|
1470
|
+
export function focusPickerMenuExpression() {
|
|
1471
|
+
return `(() => {
|
|
1472
|
+
const menu = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
|
|
1473
|
+
if (!menu) return { ok: false, reason: "picker menu not open" };
|
|
1474
|
+
// Leave the focus the menu gave itself. It opens on the checked model row,
|
|
1475
|
+
// and ArrowDown from there walks the model list. Focusing the first item
|
|
1476
|
+
// instead lands on the label row above the slider, where ArrowDown only
|
|
1477
|
+
// cycles that row's own widgets - measured as four steps that repeat
|
|
1478
|
+
// forever and never reach a radio.
|
|
1479
|
+
if (menu.contains(document.activeElement)) return { ok: true };
|
|
1480
|
+
const radios = [...menu.querySelectorAll('[role="menuitemradio"]')];
|
|
1481
|
+
const target = radios.find((r) => r.getAttribute("aria-checked") === "true") || radios[0];
|
|
1482
|
+
if (target && typeof target.focus === "function") target.focus();
|
|
1483
|
+
return { ok: menu.contains(document.activeElement) };
|
|
1484
|
+
})()`;
|
|
1485
|
+
}
|
|
1425
1486
|
export function focusPowerSliderExpression() {
|
|
1426
1487
|
return `(() => {
|
|
1427
1488
|
const slider = document.querySelector('[role="slider"]');
|
|
@@ -1680,7 +1741,45 @@ export function stepSelectionUnavailableWarning(requested, offered) {
|
|
|
1680
1741
|
return (`step_not_applied: this ChatGPT picker has no "${requested}" step, so the send used the slider's current setting.${list} ` +
|
|
1681
1742
|
'Pick one of those with --effort, or clear a saved default with `prodex setup --clear-model`.');
|
|
1682
1743
|
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Choose a model row through the menu's roving focus.
|
|
1746
|
+
*
|
|
1747
|
+
* The rows carry pointer-events: none and sit outside the menu's box, so
|
|
1748
|
+
* verifiedClickAt refuses them and .focus() is declined. ArrowDown from the row
|
|
1749
|
+
* the menu opens on does reach them, and Enter commits. Doing this BEFORE the
|
|
1750
|
+
* slider matters: with the recommended set active the slider walks a short
|
|
1751
|
+
* mixed ladder ending at Extra High, and picking the model swaps in that
|
|
1752
|
+
* model's own ladder, which continues past it.
|
|
1753
|
+
*/
|
|
1754
|
+
async function selectPickerModelByKeyboard(cdp, candidates) {
|
|
1755
|
+
const focused = await cdp.evaluate(focusPickerMenuExpression());
|
|
1756
|
+
if (!focused?.ok)
|
|
1757
|
+
return false;
|
|
1758
|
+
const limit = MENU_KEYBOARD_WALK_LIMIT;
|
|
1759
|
+
for (let pressed = 0;; pressed += 1) {
|
|
1760
|
+
const active = await cdp.evaluate(activeMenuItemExpression());
|
|
1761
|
+
const move = menuKeyboardStep({ active, requested: candidates, pressed, limit });
|
|
1762
|
+
if (move === "exhausted")
|
|
1763
|
+
return false;
|
|
1764
|
+
if (move === "select") {
|
|
1765
|
+
await dispatchMenuKey(cdp, "Enter", 13);
|
|
1766
|
+
// Committing closes the menu. Waiting for it to finish closing keeps the
|
|
1767
|
+
// reopen below from landing mid-animation and toggling it shut again.
|
|
1768
|
+
await waitForExpressionTrue(cdp, pickerClosedExpression(), 3_000);
|
|
1769
|
+
return true;
|
|
1770
|
+
}
|
|
1771
|
+
await dispatchMenuKey(cdp, "ArrowDown", 40);
|
|
1772
|
+
await sleep(140);
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
async function dispatchMenuKey(cdp, key, code) {
|
|
1776
|
+
for (const type of ["keyDown", "keyUp"]) {
|
|
1777
|
+
await cdp.send("Input.dispatchKeyEvent", { type, key, code: key, windowsVirtualKeyCode: code });
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1683
1780
|
async function selectPickerModel(cdp, requested, warnings = []) {
|
|
1781
|
+
if (await selectPickerModelByKeyboard(cdp, [requested]))
|
|
1782
|
+
return;
|
|
1684
1783
|
const hit = await cdp.evaluate(menuItemRectExpression(requested));
|
|
1685
1784
|
if (!hit.ok || hit.x === undefined || hit.y === undefined) {
|
|
1686
1785
|
const unavailable = modelSelectionUnavailableWarning(requested, hit.reason ?? "", hit.available);
|
|
@@ -1692,6 +1791,56 @@ async function selectPickerModel(cdp, requested, warnings = []) {
|
|
|
1692
1791
|
}
|
|
1693
1792
|
await verifiedClickWithRetry(cdp, () => cdp.evaluate(menuItemRectExpression(requested)), requested);
|
|
1694
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Make sure the composer picker is open, reopening it if it is not.
|
|
1796
|
+
*
|
|
1797
|
+
* Committing a model row closes the menu, and the slider lives inside it - so
|
|
1798
|
+
* without this the step that follows reported "the picker did not expose its
|
|
1799
|
+
* power slider" on a picker that was simply shut.
|
|
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
|
+
}
|
|
1828
|
+
async function ensurePickerOpen(cdp) {
|
|
1829
|
+
// The slider, not the menu, is what the caller needs - and an open menu that
|
|
1830
|
+
// has not painted its slider yet looks identical to one that has.
|
|
1831
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
1832
|
+
if (await cdp.evaluate(powerSliderPresentExpression()))
|
|
1833
|
+
return true;
|
|
1834
|
+
await waitForExpressionTrue(cdp, pickerClosedExpression(), 1_500);
|
|
1835
|
+
const button = await cdp.evaluate(modelButtonRectExpression());
|
|
1836
|
+
if (!button.ok || button.x === undefined || button.y === undefined)
|
|
1837
|
+
return false;
|
|
1838
|
+
await verifiedClickAt(cdp, button.x, button.y, "model selector");
|
|
1839
|
+
if (await waitForExpressionTrue(cdp, powerSliderPresentExpression(), MENU_OPEN_TIMEOUT_MS))
|
|
1840
|
+
return true;
|
|
1841
|
+
}
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
1695
1844
|
async function selectPowerStep(cdp, requested) {
|
|
1696
1845
|
// The menu paints a moment after it opens, and on a page that has just been
|
|
1697
1846
|
// built - a project created seconds ago - that moment is longer. Failing the
|
|
@@ -1764,6 +1913,11 @@ export function sliderWalkPlan(state) {
|
|
|
1764
1913
|
async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
1765
1914
|
if (!options.model && !options.proMode && !options.effort)
|
|
1766
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);
|
|
1767
1921
|
// --pro-mode selects a Pro sub-mode, so it is meaningless with a non-Pro
|
|
1768
1922
|
// --model. Fail loudly instead of silently dropping the requested sub-mode.
|
|
1769
1923
|
if (options.proMode && options.model && !/pro/i.test(options.model)) {
|
|
@@ -1800,7 +1954,11 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
1800
1954
|
// the whole send). Retry briefly with FRESH coordinates; a persistent
|
|
1801
1955
|
// cover still fails with the refusal message.
|
|
1802
1956
|
const clickDeadline = Date.now() + 5_000;
|
|
1803
|
-
|
|
1957
|
+
// Clicking the trigger TOGGLES the picker, so clicking one that is already
|
|
1958
|
+
// open shuts it and the next step reports "power slider not found" about a
|
|
1959
|
+
// control that was on screen a moment earlier.
|
|
1960
|
+
const alreadyOpen = await cdp.evaluate(powerSliderPresentExpression());
|
|
1961
|
+
for (; !alreadyOpen;) {
|
|
1804
1962
|
try {
|
|
1805
1963
|
await verifiedClickAt(cdp, button.x, button.y, "model selector");
|
|
1806
1964
|
break;
|
|
@@ -1832,7 +1990,16 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
1832
1990
|
});
|
|
1833
1991
|
if (plan.warning)
|
|
1834
1992
|
selectionWarnings.push(plan.warning);
|
|
1993
|
+
// The model goes first: choosing it replaces the ladder the slider walks,
|
|
1994
|
+
// so a step picked before it would be a step of the old ladder.
|
|
1995
|
+
if (plan.modelLabel) {
|
|
1996
|
+
await selectPickerModel(cdp, plan.modelLabel, selectionWarnings);
|
|
1997
|
+
}
|
|
1835
1998
|
if (plan.sliderLabel) {
|
|
1999
|
+
// Committing the model closed the menu; the slider is inside it.
|
|
2000
|
+
if (!(await ensurePickerOpen(cdp))) {
|
|
2001
|
+
throw new Error("ChatGPT's model picker would not reopen after the model was chosen.");
|
|
2002
|
+
}
|
|
1836
2003
|
try {
|
|
1837
2004
|
await selectPowerStep(cdp, plan.sliderLabel);
|
|
1838
2005
|
}
|
|
@@ -1847,9 +2014,6 @@ async function selectModelReasoning(cdp, options, selectionWarnings = []) {
|
|
|
1847
2014
|
selectionWarnings.push(stepSelectionUnavailableWarning(plan.sliderLabel, offered));
|
|
1848
2015
|
}
|
|
1849
2016
|
}
|
|
1850
|
-
if (plan.modelLabel) {
|
|
1851
|
-
await selectPickerModel(cdp, plan.modelLabel, selectionWarnings);
|
|
1852
|
-
}
|
|
1853
2017
|
// This branch cannot honour a sub-mode, and used to return without
|
|
1854
2018
|
// saying so - the caller got a clean receipt for a 확장 it never got.
|
|
1855
2019
|
const proModeWarning = proModeNotAppliedWarning(options.proMode, sliderState.effort ?? undefined);
|
|
@@ -2839,7 +3003,11 @@ export function modelMenuOptionsExpression() {
|
|
|
2839
3003
|
return `(() => {
|
|
2840
3004
|
const m = document.querySelector('[data-testid="composer-intelligence-picker-content"]');
|
|
2841
3005
|
if (!m) return [];
|
|
2842
|
-
|
|
3006
|
+
// Only the radios are models. The plain menuitems above them are the power
|
|
3007
|
+
// slider's own rows - its track, and the label pair naming the current model
|
|
3008
|
+
// and effort - and listing those put GPT-6 Astra in twice and Extra High in
|
|
3009
|
+
// as if it were a model.
|
|
3010
|
+
return [...m.querySelectorAll('[role="menuitemradio"]')]
|
|
2843
3011
|
.map((it) => {
|
|
2844
3012
|
// A submenu row renders as label over value ("Model" / "GPT-5.6 Sol"),
|
|
2845
3013
|
// and the value is the part a person actually wants to read.
|
|
@@ -3260,44 +3428,75 @@ export async function listChatGptSidebarProjects(input = {}) {
|
|
|
3260
3428
|
* listing exactly as it was rather than failing a read-only command.
|
|
3261
3429
|
*/
|
|
3262
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;
|
|
3263
3442
|
try {
|
|
3264
|
-
const focused = await cdp.evaluate(focusPowerSliderExpression());
|
|
3265
|
-
if (!focused?.ok)
|
|
3266
|
-
return undefined;
|
|
3267
|
-
const start = await cdp.evaluate(powerSliderStateExpression());
|
|
3268
|
-
if (!start?.ok)
|
|
3269
|
-
return undefined;
|
|
3270
|
-
const plan = sliderWalkPlan({ position: start.position, min: start.min, max: start.max });
|
|
3271
|
-
if (!plan)
|
|
3272
|
-
return undefined;
|
|
3273
|
-
const current = start.effort ?? null;
|
|
3274
3443
|
for (let i = 0; i < plan.toBottom; i += 1) {
|
|
3275
3444
|
await dispatchArrowKey(cdp, "ArrowLeft");
|
|
3276
3445
|
await sleep(120);
|
|
3277
3446
|
}
|
|
3278
|
-
|
|
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.
|
|
3450
|
+
const rungs = [];
|
|
3279
3451
|
const record = async () => {
|
|
3280
3452
|
const state = await cdp.evaluate(powerSliderStateExpression());
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3453
|
+
if (!state?.ok)
|
|
3454
|
+
return;
|
|
3455
|
+
const effort = state.effort?.trim();
|
|
3456
|
+
if (!effort)
|
|
3457
|
+
return;
|
|
3458
|
+
rungs.push({ position: state.position ?? rungs.length, model: state.model?.trim() || null, effort });
|
|
3284
3459
|
};
|
|
3285
3460
|
await record();
|
|
3286
3461
|
for (let i = 0; i < plan.climb; i += 1) {
|
|
3287
3462
|
await dispatchArrowKey(cdp, "ArrowRight");
|
|
3288
|
-
await sleep(
|
|
3463
|
+
await sleep(220);
|
|
3289
3464
|
await record();
|
|
3290
3465
|
}
|
|
3291
|
-
|
|
3292
|
-
for (let i = 0; i < plan.climb - plan.back; i += 1) {
|
|
3293
|
-
await dispatchArrowKey(cdp, "ArrowLeft");
|
|
3294
|
-
await sleep(120);
|
|
3295
|
-
}
|
|
3296
|
-
return labels.length > 0 ? { labels, current } : undefined;
|
|
3466
|
+
return rungs.length > 0 ? { rungs, current } : undefined;
|
|
3297
3467
|
}
|
|
3298
3468
|
catch {
|
|
3299
3469
|
return undefined;
|
|
3300
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
|
+
}
|
|
3301
3500
|
}
|
|
3302
3501
|
export async function listChatGptModelOptions(input = {}) {
|
|
3303
3502
|
const port = resolveCdpPort(input.port);
|
|
@@ -3333,7 +3532,18 @@ export async function listChatGptModelOptions(input = {}) {
|
|
|
3333
3532
|
throw new Error("ChatGPT model menu did not open after clicking the selector");
|
|
3334
3533
|
const options = await cdp.evaluate(modelMenuOptionsExpression());
|
|
3335
3534
|
const effortSteps = await readEffortSteps(cdp);
|
|
3336
|
-
|
|
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 } : {}) };
|
|
3337
3547
|
}
|
|
3338
3548
|
finally {
|
|
3339
3549
|
try {
|
package/dist/cli-pro.js
CHANGED
|
@@ -443,17 +443,27 @@ 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));
|
|
449
452
|
}
|
|
450
453
|
if (listed.effortSteps) {
|
|
451
454
|
io.stdout("");
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
+
// One slider walks a ladder of model-and-effort pairs, so the same
|
|
456
|
+
// effort name appears at more than one rung with a different model
|
|
457
|
+
// behind it. Printing bare names hid that.
|
|
458
|
+
io.stdout("Power slider on this account (the slider was walked and put back):");
|
|
459
|
+
const rungs = listed.effortSteps.rungs;
|
|
460
|
+
const width = String(rungs.length).length;
|
|
461
|
+
for (const rung of rungs) {
|
|
462
|
+
const here = rung.effort === listed.effortSteps.current ? "*" : " ";
|
|
463
|
+
const step = String(rung.position + 1).padStart(width, " ");
|
|
464
|
+
io.stdout(`${here} ${step}/${rungs.length} ${rung.model ?? "?"} - ${rung.effort}`);
|
|
455
465
|
}
|
|
456
|
-
io.stdout("
|
|
466
|
+
io.stdout("Each row is one position of that single slider: it sets the model and the effort together, so they cannot be chosen apart. The list is complete - the slider shows one position at a time, so reading it any other way sees only the current one.");
|
|
457
467
|
}
|
|
458
468
|
else {
|
|
459
469
|
io.stdout("An arrow shows what that row is set to now; --model / --effort reach into those submenus (e.g. --model Pro).");
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,7 @@ const BrowserDefaultsSchema = z.object({
|
|
|
11
11
|
model: z.string().min(1).optional(),
|
|
12
12
|
pro_mode: z.enum(["기본", "확장"]).optional(),
|
|
13
13
|
// Pro is the slider's fifth step, so a saved default may name it.
|
|
14
|
-
effort: z.enum(["즉시", "중간", "높음", "매우 높음", "Pro"]).optional(),
|
|
14
|
+
effort: z.enum(["즉시", "중간", "높음", "매우 높음", "Max", "Ultra", "Pro"]).optional(),
|
|
15
15
|
project: z.string().min(1).optional()
|
|
16
16
|
});
|
|
17
17
|
const LocalConfigSchema = z.object({
|
|
@@ -10,6 +10,7 @@ export function readPowerSliderSelection(snapshot) {
|
|
|
10
10
|
}));
|
|
11
11
|
const powerLabels = new Set([
|
|
12
12
|
"instant",
|
|
13
|
+
"light",
|
|
13
14
|
"즉시",
|
|
14
15
|
"빠름",
|
|
15
16
|
"fast",
|
|
@@ -29,12 +30,70 @@ export function readPowerSliderSelection(snapshot) {
|
|
|
29
30
|
const legacyModel = items.find((item) => item.lines[0] === "Model")?.lines[1] ?? null;
|
|
30
31
|
const legacyEffort = items.find((item) => item.lines[0] === "Effort")?.lines[1] ?? null;
|
|
31
32
|
const checkedModel = items.find((item) => item.role === "menuitemradio" && item.checked)?.lines[0] ?? null;
|
|
32
|
-
|
|
33
|
+
// The slider stopped being an effort control: one slider now walks a ladder of
|
|
34
|
+
// model-and-effort pairs, and a row above the track reads "<model>" then
|
|
35
|
+
// "<effort>". Measured live, that pair sits in a SIBLING row - the row that
|
|
36
|
+
// literally contains the slider carries no text - so the pair is found by its
|
|
37
|
+
// shape rather than by ownership of the track. Reading the pair's first line
|
|
38
|
+
// as the effort is what made the listing announce "Extra High" as a model.
|
|
39
|
+
const pairRow = items.find((item) => item.role === "menuitem" && item.lines.length >= 2 && item.lines[0] !== "Model" && item.lines[0] !== "Effort");
|
|
40
|
+
const owner = items.find((item) => item.role === "menuitem" && item.containsSlider);
|
|
41
|
+
const sliderOwnerModel = pairRow?.lines[0] ?? null;
|
|
42
|
+
// A single-line row is the older form, where it carried only the step name.
|
|
43
|
+
const sliderOwner = pairRow?.lines[1] ?? (owner && owner.lines.length === 1 ? owner.lines[0] : null) ?? null;
|
|
33
44
|
const visiblePowerLabel = items.find((item) => item.role === "menuitem" && powerLabels.has((item.lines[0] ?? "").toLowerCase()))?.lines[0] ?? null;
|
|
34
45
|
const accessibleCandidate = snapshot.sliderValueText?.trim() || null;
|
|
35
46
|
const accessibleValue = accessibleCandidate && powerLabels.has(accessibleCandidate.toLowerCase()) ? accessibleCandidate : null;
|
|
36
47
|
return {
|
|
37
|
-
|
|
48
|
+
// The checked radio is "Default" - the recommended set - while the slider
|
|
49
|
+
// decides the model a send actually uses, so the slider's row wins.
|
|
50
|
+
model: legacyModel ?? sliderOwnerModel ?? checkedModel,
|
|
38
51
|
effort: accessibleValue ?? legacyEffort ?? sliderOwner ?? visiblePowerLabel
|
|
39
52
|
};
|
|
40
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Decide the next move when walking the composer picker's menu by keyboard.
|
|
56
|
+
*
|
|
57
|
+
* The model rows carry pointer-events: none and refuse focus(), so the menu's
|
|
58
|
+
* own roving focus is the only way to reach them. Only a radio counts as the
|
|
59
|
+
* target: the row above the slider repeats the model name as its first line,
|
|
60
|
+
* and committing there selects nothing.
|
|
61
|
+
*/
|
|
62
|
+
export function menuKeyboardStep(input) {
|
|
63
|
+
const { active, requested, pressed, limit } = input;
|
|
64
|
+
if (!active)
|
|
65
|
+
return "exhausted";
|
|
66
|
+
if (active.role === "menuitemradio") {
|
|
67
|
+
const label = active.label.trim().toLowerCase();
|
|
68
|
+
if (requested.some((candidate) => candidate.trim().toLowerCase() === label))
|
|
69
|
+
return "select";
|
|
70
|
+
}
|
|
71
|
+
return pressed < limit ? "down" : "exhausted";
|
|
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
|
+
}
|