@steipete/oracle 0.17.0 → 0.17.2
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/bin/oracle-cli.js +1 -1
- package/dist/docs-site/browser-mode.html +9 -7
- package/dist/docs-site/cli-reference.html +1 -1
- package/dist/docs-site/configuration.html +1 -1
- package/dist/docs-site/manual-tests.html +2 -1
- package/dist/docs-site/mcp.html +2 -2
- package/dist/docs-site/mythical-pro-agents.html +1 -1
- package/dist/docs-site/windows-work.html +1 -1
- package/dist/docs-site/windows.html +1 -1
- package/dist/scripts/git-policy.js +0 -8
- package/dist/scripts/runner.js +1 -5
- package/dist/src/browser/actions/modelSelection.js +164 -5
- package/dist/src/browser/actions/thinkingTime.js +267 -82
- package/dist/src/browser/chromeLifecycle.js +6 -0
- package/dist/src/cli/browserConfig.js +5 -2
- package/dist/src/cli/options.js +1 -1
- package/dist/src/oracle/thinkingTime.js +15 -3
- package/dist/src/sessionManager.js +90 -9
- package/package.json +14 -14
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
|
@@ -31,7 +31,11 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
31
31
|
const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
|
|
32
32
|
const targetModelKind = inferThinkingTargetModelKind(desiredModel);
|
|
33
33
|
const observedModelKind = result && "modelKind" in result ? result.modelKind : null;
|
|
34
|
-
|
|
34
|
+
// Pro is expensive and rate-limited, so a Pro request must never degrade quietly
|
|
35
|
+
// into a cheaper tier. Requesting it explicitly (level "pro") fails closed on its
|
|
36
|
+
// own, independently of the legacy Pro-model + "extended" combination.
|
|
37
|
+
const strictProEffort = level === "pro" ||
|
|
38
|
+
((targetModelKind === "pro" || observedModelKind === "pro") && level === "extended");
|
|
35
39
|
switch (result?.status) {
|
|
36
40
|
case "already-selected":
|
|
37
41
|
logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} (already selected)`));
|
|
@@ -53,16 +57,24 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
53
57
|
: "";
|
|
54
58
|
const message = `Thinking time: ${result.status.replaceAll("-", " ")}${kindHint} (requested ${capitalizedLevel})`;
|
|
55
59
|
if (strictProEffort) {
|
|
56
|
-
|
|
60
|
+
const target = level === "pro" ? "Pro" : "Pro Extended";
|
|
61
|
+
throw new Error(`${message}; refusing to submit without confirmed ${target}.`);
|
|
57
62
|
}
|
|
58
|
-
|
|
63
|
+
// "selection-unverified" is the one status here that already dispatched a
|
|
64
|
+
// click, so the effort may or may not have moved. Every other status left
|
|
65
|
+
// the tab on whatever effort it had — which is not necessarily the default.
|
|
66
|
+
const outcome = result.status === "selection-unverified"
|
|
67
|
+
? "the effort in ChatGPT is unconfirmed"
|
|
68
|
+
: "keeping the effort already selected in ChatGPT";
|
|
69
|
+
logger(formatBrowserThinkingLog(`${message}; ${outcome}.`));
|
|
59
70
|
return;
|
|
60
71
|
}
|
|
61
72
|
default: {
|
|
62
73
|
await logDomFailure(Runtime, logger, "thinking-time-unknown");
|
|
63
74
|
logPickerDiagnostic(result, logger);
|
|
64
75
|
if (strictProEffort) {
|
|
65
|
-
|
|
76
|
+
const target = level === "pro" ? "Pro" : "Pro Extended";
|
|
77
|
+
throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed ${target}.`);
|
|
66
78
|
}
|
|
67
79
|
logger(formatBrowserThinkingLog(`unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`));
|
|
68
80
|
return;
|
|
@@ -72,7 +84,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
72
84
|
/**
|
|
73
85
|
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
74
86
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
75
|
-
* @param level - The thinking time intensity: 'light', 'standard', 'extended', or 'heavy'
|
|
87
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', 'extra-high', 'pro', or 'heavy'
|
|
76
88
|
*/
|
|
77
89
|
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desiredModel) {
|
|
78
90
|
try {
|
|
@@ -135,14 +147,20 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
135
147
|
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
136
148
|
const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
|
|
137
149
|
|
|
138
|
-
//
|
|
150
|
+
// Multilingual matchers: English level token + observed German/Chinese variants.
|
|
139
151
|
const LEVEL_TOKENS = {
|
|
140
|
-
light: ['light', 'instant', '轻', '极速'],
|
|
141
|
-
standard: ['standard', 'medium', '标准', '中'],
|
|
142
|
-
extended: ['extended', 'high', '扩展', '深度', '加强', '高'],
|
|
143
|
-
|
|
152
|
+
light: ['light', 'instant', 'sofort', 'leicht', '轻', '极速'],
|
|
153
|
+
standard: ['standard', 'medium', 'mittel', '标准', '中'],
|
|
154
|
+
extended: ['extended', 'high', 'hoch', 'erweitert', '扩展', '深度', '加强', '高'],
|
|
155
|
+
'extra-high': ['extra high', 'sehr hoch', '极高'],
|
|
156
|
+
heavy: ['heavy', 'schwer', '重度', '加重'],
|
|
144
157
|
};
|
|
145
|
-
|
|
158
|
+
// Pro is a tier you can request, but it is also a MODEL name, so it must never
|
|
159
|
+
// be used to decide whether a control or a menu is an effort owner: a model pill
|
|
160
|
+
// reading "Pro" would be claimed as the effort pill, and a model menu listing
|
|
161
|
+
// "Instant"/"Pro" would look like a tier list. Keep it to target matching only.
|
|
162
|
+
const TARGET_LEVEL_TOKENS = { ...LEVEL_TOKENS, pro: ['pro'] };
|
|
163
|
+
const targetTokens = TARGET_LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
|
|
146
164
|
|
|
147
165
|
const INITIAL_WAIT_MS = 150;
|
|
148
166
|
const STEP_WAIT_MS = 200;
|
|
@@ -155,19 +173,43 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
155
173
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
156
174
|
// Keep CJK characters so we can match Chinese labels against LEVEL_TOKENS.
|
|
157
175
|
const normalize = (value) => (value || '')
|
|
176
|
+
// Compose first so NFD umlauts fold too, then map them onto ASCII before
|
|
177
|
+
// the strip below would drop them (and split the token in half).
|
|
178
|
+
.normalize('NFC')
|
|
158
179
|
.toLowerCase()
|
|
180
|
+
.replace(/ä/g, 'a')
|
|
181
|
+
.replace(/ö/g, 'o')
|
|
182
|
+
.replace(/ü/g, 'u')
|
|
183
|
+
.replace(/ß/g, 'ss')
|
|
159
184
|
.replace(/[^a-z0-9\\u4e00-\\u9fa5]+/g, ' ')
|
|
160
185
|
.replace(/\\s+/g, ' ')
|
|
161
186
|
.trim();
|
|
162
187
|
const hasToken = (text, token) => normalize(text).split(' ').includes(token);
|
|
163
|
-
|
|
188
|
+
// Whole-word/phrase containment. Latin effort labels are short words that also
|
|
189
|
+
// occur inside unrelated UI text ("Hochladen", "Ermitteln") and inside their own
|
|
190
|
+
// row descriptions ("Hoch – für sehr komplexe Aufgaben"), so plain substring
|
|
191
|
+
// matching misclassifies rows. CJK labels have no word separators, so they keep
|
|
192
|
+
// substring semantics.
|
|
193
|
+
const hasPhrase = (text, phrase) => {
|
|
194
|
+
const haystack = ' ' + normalize(text) + ' ';
|
|
195
|
+
const needle = normalize(phrase);
|
|
196
|
+
if (!needle) return false;
|
|
197
|
+
return /^[a-z0-9 ]+$/.test(needle)
|
|
198
|
+
? haystack.includes(' ' + needle + ' ')
|
|
199
|
+
: haystack.includes(needle);
|
|
200
|
+
};
|
|
201
|
+
// ChatGPT's Pro effort tiers are "Pro Extended"/"Pro Erweitert" per UI language.
|
|
202
|
+
const hasExtendedWord = (text) => hasPhrase(text, 'extended') || hasPhrase(text, 'erweitert');
|
|
203
|
+
const matchesTokens = (text, tokens) => {
|
|
164
204
|
const t = normalize(text);
|
|
165
205
|
if (!t) return false;
|
|
166
|
-
return
|
|
206
|
+
return tokens.some((tok) => {
|
|
167
207
|
const token = normalize(tok);
|
|
168
208
|
if (!token) return false;
|
|
169
|
-
if (token === 'high') return
|
|
170
|
-
if (token === 'extra high') return
|
|
209
|
+
if (token === 'high') return hasPhrase(t, 'high') && !hasPhrase(t, 'extra high');
|
|
210
|
+
if (token === 'extra high') return hasPhrase(t, 'extra high');
|
|
211
|
+
if (token === 'hoch') return hasPhrase(t, 'hoch') && !hasPhrase(t, 'sehr hoch');
|
|
212
|
+
if (token === 'sehr hoch') return hasPhrase(t, 'sehr hoch');
|
|
171
213
|
if (token === '极速') {
|
|
172
214
|
const suffix = t.slice(token.length);
|
|
173
215
|
return t === token || hasToken(t, token) || /^[0-9]/.test(suffix);
|
|
@@ -175,27 +217,15 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
175
217
|
if (['中', '高', '极高'].includes(token)) {
|
|
176
218
|
return t === token || hasToken(t, token);
|
|
177
219
|
}
|
|
220
|
+
if (/^[a-z0-9 ]+$/.test(token)) {
|
|
221
|
+
return hasPhrase(t, token);
|
|
222
|
+
}
|
|
178
223
|
return t === token || hasToken(t, token) || t.includes(token);
|
|
179
224
|
});
|
|
180
225
|
};
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
for (const tokens of Object.values(LEVEL_TOKENS)) {
|
|
185
|
-
for (const rawToken of tokens) {
|
|
186
|
-
const token = normalize(rawToken);
|
|
187
|
-
if (!token) continue;
|
|
188
|
-
if (token.includes(' ')) {
|
|
189
|
-
if (token.split(' ').every((part) => hasToken(normalizedText, part))) return true;
|
|
190
|
-
} else if (/^[a-z0-9]+$/.test(token)) {
|
|
191
|
-
if (hasToken(normalizedText, token)) return true;
|
|
192
|
-
} else if (normalizedText.includes(token)) {
|
|
193
|
-
return true;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return false;
|
|
198
|
-
};
|
|
226
|
+
const matchesLevel = (text) => matchesTokens(text, targetTokens);
|
|
227
|
+
const matchesAnyEffortLevel = (text) =>
|
|
228
|
+
Object.values(LEVEL_TOKENS).some((tokens) => matchesTokens(text, tokens));
|
|
199
229
|
const optionIsSelected = (node) => {
|
|
200
230
|
if (!(node instanceof HTMLElement)) return false;
|
|
201
231
|
const ariaChecked = node.getAttribute('aria-checked');
|
|
@@ -347,7 +377,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
347
377
|
return true;
|
|
348
378
|
}
|
|
349
379
|
const label = menu?.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
350
|
-
|
|
380
|
+
// 'intelligen' matches both "Intelligence" and German "Intelligenz".
|
|
381
|
+
return normalize(label?.textContent ?? '').includes('intelligen');
|
|
351
382
|
};
|
|
352
383
|
const failure = (status, extra = {}) => ({
|
|
353
384
|
status,
|
|
@@ -356,7 +387,20 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
356
387
|
diagnostic: collectPickerDiagnostic(),
|
|
357
388
|
});
|
|
358
389
|
const findOptionInMenu = (menu, modelKindOverride = null) => {
|
|
359
|
-
|
|
390
|
+
// Container controls reveal other controls; they are not tiers you can pick.
|
|
391
|
+
// Two shapes exist and both can collide with a tier label: a submenu opener
|
|
392
|
+
// ("ModelGPT-5.6 Pro" would satisfy a Pro request) and a disclosure toggle
|
|
393
|
+
// (German "Erweitert" is literally one of the extended tokens, so the
|
|
394
|
+
// Advanced toggle would satisfy an extended request and be clicked in place
|
|
395
|
+
// of the tier). Detect them structurally rather than by label: a real tier row
|
|
396
|
+
// carries a checked state, while a container carries expansion state.
|
|
397
|
+
const isContainerControl = (node) =>
|
|
398
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' ||
|
|
399
|
+
(node?.getAttribute?.('aria-expanded') !== null &&
|
|
400
|
+
node?.getAttribute?.('aria-checked') === null);
|
|
401
|
+
const items = Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(
|
|
402
|
+
(item) => !isContainerControl(item),
|
|
403
|
+
);
|
|
360
404
|
const modelKind = modelKindOverride || effectiveTargetModelKind();
|
|
361
405
|
if (modelKind === 'pro') {
|
|
362
406
|
// GPT-5.6's unified Intelligence picker exposes Pro as the highest
|
|
@@ -390,29 +434,19 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
390
434
|
return null;
|
|
391
435
|
}
|
|
392
436
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
) {
|
|
398
|
-
for (const item of items) {
|
|
399
|
-
const itemText = normalize(
|
|
400
|
-
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
401
|
-
);
|
|
402
|
-
if (
|
|
403
|
-
hasToken(itemText, 'pro') &&
|
|
404
|
-
!itemText.includes('gpt') &&
|
|
405
|
-
!/(?:^|\\s)5[ .-]?6(?:\\s|$)/.test(itemText)
|
|
406
|
-
) {
|
|
407
|
-
return item;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
437
|
+
// Generic effort-label match for every model/level. GPT-5.6 heavy used to
|
|
438
|
+
// short-circuit to the Pro row before reaching here; it no longer does, so
|
|
439
|
+
// a UI without a matching tier (e.g. German, which has no "heavy") falls
|
|
440
|
+
// through to null and the caller keeps the current selection.
|
|
411
441
|
for (const item of items) {
|
|
412
442
|
const itemText = normalize(
|
|
413
443
|
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
414
444
|
);
|
|
415
|
-
|
|
445
|
+
// Pro rows are skipped for non-Pro targets so "High" never resolves to
|
|
446
|
+
// "Pro". Two things lift this: an explicit TARGET_LEVEL of 'pro', and a
|
|
447
|
+
// legacy Pro-model menu (modelKind === 'pro'), whose rows are all Pro
|
|
448
|
+
// variants so excluding them would leave nothing to match.
|
|
449
|
+
if (TARGET_LEVEL !== 'pro' && modelKind !== 'pro' && hasToken(itemText, 'pro')) {
|
|
416
450
|
continue;
|
|
417
451
|
}
|
|
418
452
|
if (
|
|
@@ -422,10 +456,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
422
456
|
return item;
|
|
423
457
|
}
|
|
424
458
|
}
|
|
425
|
-
if (TARGET_LEVEL === '
|
|
426
|
-
// Older Chinese layouts used bare 高 for the highest effort.
|
|
427
|
-
// only as a second-pass exact fallback so a current 高 row can
|
|
428
|
-
// win before the primary 极高 row.
|
|
459
|
+
if (TARGET_LEVEL === 'extra-high') {
|
|
460
|
+
// Older Chinese layouts used bare 高 for the highest non-Pro effort.
|
|
461
|
+
// Keep it only as a second-pass exact fallback so a current 高 row can
|
|
462
|
+
// never win before the primary 极高 row.
|
|
429
463
|
for (const item of items) {
|
|
430
464
|
const itemText = normalize(item.textContent ?? '');
|
|
431
465
|
const ariaLabel = normalize(item.getAttribute?.('aria-label') ?? '');
|
|
@@ -434,11 +468,15 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
434
468
|
}
|
|
435
469
|
return null;
|
|
436
470
|
};
|
|
471
|
+
// Menu-shape heuristic only. This reads the whole menu's textContent, where
|
|
472
|
+
// adjacent row labels concatenate without a separator ("Pro StandardPro
|
|
473
|
+
// Extended"), so word-boundary matching does not apply here — substring is
|
|
474
|
+
// deliberate. Row-level classification uses matchesLevel/matchesTokens.
|
|
437
475
|
const countEffortLevels = (menu) => {
|
|
438
476
|
const text = normalize(menu?.textContent ?? '');
|
|
439
477
|
let hits = 0;
|
|
440
478
|
for (const tokens of Object.values(LEVEL_TOKENS)) {
|
|
441
|
-
if (tokens.some((token) => text.includes(
|
|
479
|
+
if (tokens.some((token) => text.includes(normalize(token)))) hits += 1;
|
|
442
480
|
}
|
|
443
481
|
return hits;
|
|
444
482
|
};
|
|
@@ -449,16 +487,22 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
449
487
|
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
450
488
|
const labelText = normalize(label?.textContent ?? '');
|
|
451
489
|
return (
|
|
452
|
-
labelText.includes('
|
|
490
|
+
labelText.includes('intelligen') ||
|
|
453
491
|
labelText.includes('thinking time') ||
|
|
454
492
|
labelText.includes('thinking effort') ||
|
|
493
|
+
labelText.includes('denkdauer') ||
|
|
494
|
+
labelText.includes('denkzeit') ||
|
|
455
495
|
countEffortLevels(menu) >= 2
|
|
456
496
|
);
|
|
457
497
|
};
|
|
458
498
|
const isProEffortMenu = (menu) => {
|
|
459
499
|
if (!isVisible(menu)) return false;
|
|
460
500
|
const text = normalize(menu?.textContent ?? '');
|
|
461
|
-
|
|
501
|
+
// Aggregate menu text, so plain substring only (see countEffortLevels).
|
|
502
|
+
return (
|
|
503
|
+
text.includes('pro standard') &&
|
|
504
|
+
(text.includes('pro extended') || text.includes('pro erweitert'))
|
|
505
|
+
);
|
|
462
506
|
};
|
|
463
507
|
const controlledMenu = (trigger) => {
|
|
464
508
|
const id = trigger?.getAttribute?.('aria-controls');
|
|
@@ -493,10 +537,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
493
537
|
(node?.textContent ?? '') + ' ' + (node?.getAttribute?.('aria-label') ?? ''),
|
|
494
538
|
);
|
|
495
539
|
if (TARGET_LEVEL === 'standard') {
|
|
496
|
-
return text
|
|
540
|
+
return hasPhrase(text, 'pro') && hasPhrase(text, 'standard');
|
|
497
541
|
}
|
|
498
542
|
if (TARGET_LEVEL === 'extended') {
|
|
499
|
-
return text
|
|
543
|
+
return hasPhrase(text, 'pro') && hasExtendedWord(text);
|
|
500
544
|
}
|
|
501
545
|
return false;
|
|
502
546
|
};
|
|
@@ -521,10 +565,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
521
565
|
}
|
|
522
566
|
const label = normalize(button?.textContent ?? '');
|
|
523
567
|
if (TARGET_LEVEL === 'standard') {
|
|
524
|
-
return hasToken(label, 'pro') && !
|
|
568
|
+
return hasToken(label, 'pro') && !hasExtendedWord(label);
|
|
525
569
|
}
|
|
526
570
|
if (TARGET_LEVEL === 'extended') {
|
|
527
|
-
return hasToken(label, 'pro') &&
|
|
571
|
+
return hasToken(label, 'pro') && hasExtendedWord(label);
|
|
528
572
|
}
|
|
529
573
|
return false;
|
|
530
574
|
};
|
|
@@ -534,13 +578,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
534
578
|
const normalizedLabel = normalize(
|
|
535
579
|
(button?.textContent ?? '') + ' ' + (button?.getAttribute?.('aria-label') ?? ''),
|
|
536
580
|
);
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
hasToken(normalizedLabel, 'pro')
|
|
541
|
-
) {
|
|
542
|
-
return true;
|
|
543
|
-
}
|
|
581
|
+
// No 5.6-heavy "a Pro pill counts as heavy" shortcut here: that would also
|
|
582
|
+
// make post-click verification pass on an unchanged Pro pill. selectAndVerify
|
|
583
|
+
// handles the already-on-Pro case explicitly before any click.
|
|
544
584
|
if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) === 'pro') {
|
|
545
585
|
return false;
|
|
546
586
|
}
|
|
@@ -553,14 +593,21 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
553
593
|
modelKindFromNode(trigger) ||
|
|
554
594
|
effectiveTargetModelKind();
|
|
555
595
|
const option = findOption();
|
|
556
|
-
if (
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
596
|
+
if (!option && TARGET_IS_GPT56_MODEL && TARGET_LEVEL === 'heavy') {
|
|
597
|
+
// GPT-5.6 has no "heavy" tier: Pro is the closest thing. Accept a pill that
|
|
598
|
+
// is already on Pro as satisfying the request, but never click Pro to get
|
|
599
|
+
// there, and never let this stand in for post-click verification.
|
|
600
|
+
const pill = freshComposerTrigger(trigger) || findModelButton();
|
|
601
|
+
const pillLabel = normalize(
|
|
602
|
+
(pill?.textContent ?? '') + ' ' + (pill?.getAttribute?.('aria-label') ?? ''),
|
|
603
|
+
);
|
|
604
|
+
if (
|
|
605
|
+
hasToken(pillLabel, 'pro') ||
|
|
606
|
+
currentEffortPillMatchesTarget(trigger, triggerModelKind)
|
|
607
|
+
) {
|
|
608
|
+
closeOpenMenus();
|
|
609
|
+
return { status: 'already-selected', label: trigger.textContent?.trim?.() || null };
|
|
610
|
+
}
|
|
564
611
|
}
|
|
565
612
|
if (!option) return failure('option-not-found', { modelKind: triggerModelKind });
|
|
566
613
|
const label = option.textContent?.trim?.() || null;
|
|
@@ -629,6 +676,119 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
629
676
|
return null;
|
|
630
677
|
};
|
|
631
678
|
|
|
679
|
+
// ---------- Unified Intelligence picker: Advanced -> Effort submenu ----------
|
|
680
|
+
// A newer ChatGPT layout replaces the flat effort rows with a "power" slider
|
|
681
|
+
// (simple view) plus an "Advanced" view holding two submenu openers: Model and
|
|
682
|
+
// Effort. The tier rows only exist inside the Effort submenu, so the flat scan
|
|
683
|
+
// of the top-level menu finds nothing and we must expand and descend.
|
|
684
|
+
const ADVANCED_VIEW_SELECTOR = '[data-testid="composer-model-picker-slider-advanced-view"]';
|
|
685
|
+
const SUBMENU_OPENER_SELECTOR = '[role="menuitem"][aria-haspopup="menu"]';
|
|
686
|
+
const nodeLabel = (node) =>
|
|
687
|
+
normalize((node?.getAttribute?.('aria-label') ?? '') + ' ' + (node?.textContent ?? ''));
|
|
688
|
+
// Row labels in this menu concatenate without separators ("EffortHigh"), so
|
|
689
|
+
// token matching cannot be used here — substring is deliberate, as in
|
|
690
|
+
// countEffortLevels above.
|
|
691
|
+
const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', 'avanzado', 'avancado', 'avance'];
|
|
692
|
+
const EFFORT_WORDS = [
|
|
693
|
+
'effort', 'aufwand', '强度', '努力',
|
|
694
|
+
'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
|
|
695
|
+
];
|
|
696
|
+
const containsAny = (label, words) => words.some((word) => label.includes(word));
|
|
697
|
+
const findAdvancedToggle = (menu) => {
|
|
698
|
+
for (const item of (menu || document).querySelectorAll('[role="menuitem"]')) {
|
|
699
|
+
if (!isVisible(item)) continue;
|
|
700
|
+
if (containsAny(nodeLabel(item), ADVANCED_WORDS)) return item;
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
};
|
|
704
|
+
// Verify the shape rather than trusting the selector: a tier row must never be
|
|
705
|
+
// mistaken for the opener, or hovering it would silently change the effort.
|
|
706
|
+
const isSubmenuOpener = (node) =>
|
|
707
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' &&
|
|
708
|
+
(node?.getAttribute?.('role') ?? '') === 'menuitem';
|
|
709
|
+
// The Effort opener's label is the word "Effort" plus the tier it currently sits
|
|
710
|
+
// on ("EffortHigh"). Positive identification only: the sibling Model opener has
|
|
711
|
+
// the identical shape and can read "ModelGPT-5.6 Pro", so guessing from tier
|
|
712
|
+
// words would pick it and then click model rows as if they were efforts. An
|
|
713
|
+
// unrecognised language yields no opener, and the caller fails instead of
|
|
714
|
+
// gambling on the wrong control.
|
|
715
|
+
const findEffortSubmenuOpener = (menu) => {
|
|
716
|
+
const scope = menu?.querySelector?.(ADVANCED_VIEW_SELECTOR) || menu || document;
|
|
717
|
+
for (const item of scope.querySelectorAll(SUBMENU_OPENER_SELECTOR)) {
|
|
718
|
+
if (!isVisible(item) || !isSubmenuOpener(item)) continue;
|
|
719
|
+
if (containsAny(nodeLabel(item), EFFORT_WORDS)) return item;
|
|
720
|
+
}
|
|
721
|
+
return null;
|
|
722
|
+
};
|
|
723
|
+
// countEffortLevels reads aggregate menu text, where one "Extra High" row scores
|
|
724
|
+
// twice (once as "high", once as "extra high"). Count distinct levels across
|
|
725
|
+
// distinct rows instead, so ">= 2" really means two selectable tiers.
|
|
726
|
+
const countDistinctTierRows = (menu) => {
|
|
727
|
+
if (!menu) return 0;
|
|
728
|
+
const matched = new Set();
|
|
729
|
+
for (const row of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
|
|
730
|
+
if (isSubmenuOpener(row)) continue;
|
|
731
|
+
const text = nodeLabel(row);
|
|
732
|
+
for (const [level, tokens] of Object.entries(LEVEL_TOKENS)) {
|
|
733
|
+
if (matchesTokens(text, tokens)) matched.add(level);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
return matched.size;
|
|
737
|
+
};
|
|
738
|
+
const resolveSubmenuFor = (opener, parentMenu) => {
|
|
739
|
+
const id = opener?.getAttribute?.('aria-controls');
|
|
740
|
+
if (id) {
|
|
741
|
+
const node = document.getElementById?.(id);
|
|
742
|
+
if (isVisible(node) && countDistinctTierRows(node) >= 2) return node;
|
|
743
|
+
}
|
|
744
|
+
let best = null;
|
|
745
|
+
for (const menu of document.querySelectorAll(MENU_CONTAINER_SELECTOR)) {
|
|
746
|
+
if (menu === parentMenu || menu.contains?.(opener) || !isVisible(menu)) continue;
|
|
747
|
+
const hits = countDistinctTierRows(menu);
|
|
748
|
+
if (hits >= 2 && (!best || hits > best.hits)) best = { menu, hits };
|
|
749
|
+
}
|
|
750
|
+
return best?.menu ?? null;
|
|
751
|
+
};
|
|
752
|
+
const selectEffortFromAdvancedSubmenu = async (parentMenu, modelKindOverride = null) => {
|
|
753
|
+
if (!parentMenu) return null;
|
|
754
|
+
// React can mount the advanced view well after the click under load, so poll
|
|
755
|
+
// for the opener to the same deadline the submenu gets instead of assuming it
|
|
756
|
+
// rendered within one STEP_WAIT_MS. Re-expand if the toggle collapses again.
|
|
757
|
+
let opener = null;
|
|
758
|
+
const openerDeadline = performance.now() + MAX_WAIT_MS;
|
|
759
|
+
while (!opener && performance.now() < openerDeadline) {
|
|
760
|
+
const toggle = findAdvancedToggle(parentMenu);
|
|
761
|
+
if (toggle && toggle.getAttribute?.('aria-expanded') === 'false') {
|
|
762
|
+
dispatchClickSequence(toggle);
|
|
763
|
+
await sleep(STEP_WAIT_MS);
|
|
764
|
+
}
|
|
765
|
+
opener = findEffortSubmenuOpener(parentMenu);
|
|
766
|
+
if (opener) break;
|
|
767
|
+
await sleep(100);
|
|
768
|
+
}
|
|
769
|
+
if (!opener) return null;
|
|
770
|
+
dispatchHoverSequence(opener);
|
|
771
|
+
if (opener.getAttribute?.('aria-expanded') !== 'true') {
|
|
772
|
+
dispatchClickSequence(opener);
|
|
773
|
+
}
|
|
774
|
+
const deadline = performance.now() + MAX_WAIT_MS;
|
|
775
|
+
while (performance.now() < deadline) {
|
|
776
|
+
const submenu = resolveSubmenuFor(opener, parentMenu);
|
|
777
|
+
if (submenu) {
|
|
778
|
+
return selectAndVerify(
|
|
779
|
+
opener,
|
|
780
|
+
() => {
|
|
781
|
+
const current = resolveSubmenuFor(opener, parentMenu);
|
|
782
|
+
return current ? findOptionInMenu(current, modelKindOverride) : null;
|
|
783
|
+
},
|
|
784
|
+
modelKindOverride,
|
|
785
|
+
);
|
|
786
|
+
}
|
|
787
|
+
await sleep(100);
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
790
|
+
};
|
|
791
|
+
|
|
632
792
|
// Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
|
|
633
793
|
// controlled menu contains the effort levels. Prefer this ownership boundary
|
|
634
794
|
// before probing older model-picker layouts.
|
|
@@ -725,9 +885,26 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
725
885
|
}
|
|
726
886
|
if (composerEffortPill) {
|
|
727
887
|
if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
|
|
888
|
+
// In the unified Intelligence picker the composer pill shows the current
|
|
889
|
+
// EFFORT ("Pro", "High"), not the model. Reading a Pro *model* out of it would
|
|
890
|
+
// lift the Pro-row exclusion in findOptionInMenu and let a lower-tier request
|
|
891
|
+
// settle on Pro. Only a pill naming a tier and nothing else qualifies: legacy
|
|
892
|
+
// pills read "Pro Extended" (model + effort) and must keep naming their model,
|
|
893
|
+
// or a Pro Extended user asking for extended would be moved down to High.
|
|
894
|
+
const pillLabel = normalize(
|
|
895
|
+
(composerEffortPill.getAttribute?.('aria-label') ?? '') +
|
|
896
|
+
' ' +
|
|
897
|
+
(composerEffortPill.textContent ?? ''),
|
|
898
|
+
);
|
|
899
|
+
const pillIsBareEffortTier = Object.values(TARGET_LEVEL_TOKENS).some((tokens) =>
|
|
900
|
+
tokens.some((token) => normalize(token) === pillLabel),
|
|
901
|
+
);
|
|
902
|
+
const pillNamesEffortNotModel =
|
|
903
|
+
TARGET_IS_GPT56_MODEL ||
|
|
904
|
+
(pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
|
|
728
905
|
const composerModelKind =
|
|
729
906
|
TARGET_MODEL_KIND ||
|
|
730
|
-
(
|
|
907
|
+
(pillNamesEffortNotModel ? 'versioned' : modelKindFromNode(composerEffortPill));
|
|
731
908
|
if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
|
|
732
909
|
dispatchClickSequence(composerEffortPill);
|
|
733
910
|
await sleep(INITIAL_WAIT_MS);
|
|
@@ -740,6 +917,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
740
917
|
if (proEffortResult) {
|
|
741
918
|
return proEffortResult;
|
|
742
919
|
}
|
|
920
|
+
// Flat rows win when present; only descend into Advanced -> Effort when
|
|
921
|
+
// this menu has no matching tier of its own (the slider layout).
|
|
922
|
+
if (!findOptionInMenu(menu, composerModelKind)) {
|
|
923
|
+
const advancedResult = await selectEffortFromAdvancedSubmenu(menu, composerModelKind);
|
|
924
|
+
if (advancedResult) {
|
|
925
|
+
return advancedResult;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
743
928
|
return selectAndVerify(
|
|
744
929
|
composerEffortPill,
|
|
745
930
|
() => {
|
|
@@ -857,7 +1042,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
857
1042
|
const text = normalize(
|
|
858
1043
|
(node?.textContent ?? '') + ' ' + (node?.getAttribute?.('aria-label') ?? ''),
|
|
859
1044
|
);
|
|
860
|
-
return text
|
|
1045
|
+
return hasPhrase(text, 'pro') && hasExtendedWord(text);
|
|
861
1046
|
};
|
|
862
1047
|
const findProExtendedOption = () => {
|
|
863
1048
|
const menu = document.querySelector(INTELLIGENCE_MENU_SELECTOR);
|
|
@@ -542,6 +542,12 @@ function buildChromeFlags(headless, debugBindAddress, hideWindow = false) {
|
|
|
542
542
|
"--disable-features=TranslateUI,AutomationControlled",
|
|
543
543
|
"--mute-audio",
|
|
544
544
|
"--window-size=1280,720",
|
|
545
|
+
// Chrome that *we* launch is pinned to English, so ChatGPT renders the labels
|
|
546
|
+
// our selectors were written against. This does not make English the only case
|
|
547
|
+
// to handle: --browser-attach-running and --remote-chrome never build these
|
|
548
|
+
// flags (see controlPlan.ts), so those runs inherit the user's own Chrome
|
|
549
|
+
// locale, and a ChatGPT account language setting can localize the UI even here.
|
|
550
|
+
// That is why the model/effort matchers must stay language-tolerant.
|
|
545
551
|
"--lang=en-US",
|
|
546
552
|
"--accept-lang=en-US,en",
|
|
547
553
|
];
|
|
@@ -19,7 +19,7 @@ const BROWSER_MODEL_LABELS = [
|
|
|
19
19
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
20
20
|
["gpt-5.6-sol", "GPT-5.6 Sol"],
|
|
21
21
|
["gpt-5.6", "GPT-5.6 Sol"],
|
|
22
|
-
["gpt-5.5-pro", "
|
|
22
|
+
["gpt-5.5-pro", "GPT-5.5"],
|
|
23
23
|
["gpt-5.5-instant", "GPT-5.5 Instant"],
|
|
24
24
|
["gpt-5.5", "Thinking 5.5"],
|
|
25
25
|
["gpt-5.4-pro", "Pro"],
|
|
@@ -85,8 +85,11 @@ export async function buildBrowserConfig(options) {
|
|
|
85
85
|
const normalizedOverride = desiredModelOverride?.toLowerCase() ?? "";
|
|
86
86
|
const baseModel = options.model.toLowerCase();
|
|
87
87
|
const isChatGptModel = baseModel.startsWith("gpt-") && !baseModel.includes("codex");
|
|
88
|
+
const normalizedBrowserModel = normalizeChatGptModelForBrowser(options.model);
|
|
88
89
|
const shouldUseOverride = !isChatGptModel && normalizedOverride.length > 0 && normalizedOverride !== baseModel;
|
|
89
90
|
const modelStrategy = normalizeBrowserModelStrategy(options.browserModelStrategy) ?? DEFAULT_MODEL_STRATEGY;
|
|
91
|
+
const thinkingTime = normalizeThinkingTimeLevel(options.browserThinkingTime) ??
|
|
92
|
+
(modelStrategy === "select" && normalizedBrowserModel === "gpt-5.5-pro" ? "pro" : undefined);
|
|
90
93
|
assertBrowserModelAvailable(options.model, modelStrategy);
|
|
91
94
|
const cookieNames = parseCookieNames(options.browserCookieNames ?? process.env.ORACLE_BROWSER_COOKIE_NAMES);
|
|
92
95
|
let inline = await resolveInlineCookies({
|
|
@@ -175,7 +178,7 @@ export async function buildBrowserConfig(options) {
|
|
|
175
178
|
allowCookieErrors: options.browserAllowCookieErrors ?? true,
|
|
176
179
|
remoteChrome,
|
|
177
180
|
browserTabRef: options.browserTab ?? undefined,
|
|
178
|
-
thinkingTime
|
|
181
|
+
thinkingTime,
|
|
179
182
|
researchMode: options.browserResearch === "deep" ? "deep" : "off",
|
|
180
183
|
archiveConversations: options.browserArchive,
|
|
181
184
|
};
|
package/dist/src/cli/options.js
CHANGED
|
@@ -133,7 +133,7 @@ export function parseThinkingTimeOption(value) {
|
|
|
133
133
|
if (normalized) {
|
|
134
134
|
return normalized;
|
|
135
135
|
}
|
|
136
|
-
throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "
|
|
136
|
+
throw new InvalidArgumentError('Thinking time must be one of "light", "standard", "extended", "extra-high", "pro", "heavy", or a ChatGPT UI alias like "instant", "medium", "high", or "xhigh".');
|
|
137
137
|
}
|
|
138
138
|
export function normalizeModelOption(value) {
|
|
139
139
|
return (value ?? "").trim();
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
export const THINKING_TIME_LEVELS = [
|
|
1
|
+
export const THINKING_TIME_LEVELS = [
|
|
2
|
+
"light",
|
|
3
|
+
"standard",
|
|
4
|
+
"extended",
|
|
5
|
+
"extra-high",
|
|
6
|
+
// ChatGPT's unified Intelligence picker exposes Pro as the top effort tier of
|
|
7
|
+
// the active model rather than a separate model row, so it is a level here.
|
|
8
|
+
// Kept distinct from "heavy": requesting Pro must be deliberate.
|
|
9
|
+
"pro",
|
|
10
|
+
"heavy",
|
|
11
|
+
];
|
|
2
12
|
export const THINKING_TIME_ALIASES = [
|
|
3
13
|
"instant",
|
|
4
14
|
"low",
|
|
5
15
|
"medium",
|
|
6
16
|
"high",
|
|
7
|
-
"extra-high",
|
|
8
17
|
"extra high",
|
|
9
18
|
"extrahigh",
|
|
10
19
|
"xhigh",
|
|
@@ -29,10 +38,13 @@ export function normalizeThinkingTimeLevel(value) {
|
|
|
29
38
|
case "extended":
|
|
30
39
|
case "high":
|
|
31
40
|
return "extended";
|
|
32
|
-
case "heavy":
|
|
33
41
|
case "extra-high":
|
|
34
42
|
case "extrahigh":
|
|
35
43
|
case "xhigh":
|
|
44
|
+
return "extra-high";
|
|
45
|
+
case "pro":
|
|
46
|
+
return "pro";
|
|
47
|
+
case "heavy":
|
|
36
48
|
return "heavy";
|
|
37
49
|
default:
|
|
38
50
|
return null;
|