@steipete/oracle 0.17.1 → 0.17.3
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/bin/oracle-mcp.js +0 -0
- package/dist/bin/oracle.js +683 -0
- package/dist/docs-site/bridge.html +1 -17
- package/dist/docs-site/browser-mode.html +14 -20
- package/dist/docs-site/cli-reference.html +6 -6
- package/dist/docs-site/configuration.html +7 -33
- package/dist/docs-site/followup.html +9 -16
- package/dist/docs-site/gemini.html +6 -10
- package/dist/docs-site/index.html +7 -7
- package/dist/docs-site/install.html +1 -1
- package/dist/docs-site/manual-tests.html +1 -2
- package/dist/docs-site/mcp.html +5 -28
- package/dist/docs-site/mythical-pro-agents.html +10 -10
- package/dist/docs-site/openai-endpoints.html +1 -24
- package/dist/docs-site/quickstart.html +1 -1
- package/dist/docs-site/sessions.html +5 -6
- package/dist/docs-site/spec.html +1 -1
- package/dist/docs-site/windows-work.html +0 -1
- package/dist/markdansi/types/index.js +4 -0
- package/dist/oracle/bin/oracle-cli.js +472 -0
- package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
- package/dist/oracle/src/browser/actions/attachments.js +82 -0
- package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
- package/dist/oracle/src/browser/actions/navigation.js +75 -0
- package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
- package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
- package/dist/oracle/src/browser/config.js +33 -0
- package/dist/oracle/src/browser/constants.js +40 -0
- package/dist/oracle/src/browser/cookies.js +210 -0
- package/dist/oracle/src/browser/domDebug.js +36 -0
- package/dist/oracle/src/browser/index.js +331 -0
- package/dist/oracle/src/browser/pageActions.js +5 -0
- package/dist/oracle/src/browser/prompt.js +88 -0
- package/dist/oracle/src/browser/promptSummary.js +20 -0
- package/dist/oracle/src/browser/sessionRunner.js +80 -0
- package/dist/oracle/src/browser/types.js +1 -0
- package/dist/oracle/src/browser/utils.js +62 -0
- package/dist/oracle/src/browserMode.js +1 -0
- package/dist/oracle/src/cli/browserConfig.js +44 -0
- package/dist/oracle/src/cli/dryRun.js +59 -0
- package/dist/oracle/src/cli/engine.js +17 -0
- package/dist/oracle/src/cli/errorUtils.js +9 -0
- package/dist/oracle/src/cli/help.js +70 -0
- package/dist/oracle/src/cli/markdownRenderer.js +15 -0
- package/dist/oracle/src/cli/options.js +103 -0
- package/dist/oracle/src/cli/promptRequirement.js +14 -0
- package/dist/oracle/src/cli/rootAlias.js +30 -0
- package/dist/oracle/src/cli/sessionCommand.js +77 -0
- package/dist/oracle/src/cli/sessionDisplay.js +270 -0
- package/dist/oracle/src/cli/sessionRunner.js +94 -0
- package/dist/oracle/src/heartbeat.js +43 -0
- package/dist/oracle/src/oracle/client.js +48 -0
- package/dist/oracle/src/oracle/config.js +29 -0
- package/dist/oracle/src/oracle/errors.js +101 -0
- package/dist/oracle/src/oracle/files.js +220 -0
- package/dist/oracle/src/oracle/format.js +33 -0
- package/dist/oracle/src/oracle/fsAdapter.js +7 -0
- package/dist/oracle/src/oracle/oscProgress.js +60 -0
- package/dist/oracle/src/oracle/request.js +48 -0
- package/dist/oracle/src/oracle/run.js +444 -0
- package/dist/oracle/src/oracle/tokenStats.js +39 -0
- package/dist/oracle/src/oracle/types.js +1 -0
- package/dist/oracle/src/oracle.js +9 -0
- package/dist/oracle/src/sessionManager.js +205 -0
- package/dist/oracle/src/version.js +39 -0
- package/dist/scripts/chrome/browser-tools.js +295 -0
- package/dist/scripts/git-policy.js +0 -8
- package/dist/scripts/runner.js +1 -5
- package/dist/src/browser/actions/assistantResponse.js +71 -23
- package/dist/src/browser/actions/modelSelection.js +164 -5
- package/dist/src/browser/actions/thinkingTime.js +195 -18
- package/dist/src/browser/chromeCookies.js +312 -0
- package/dist/src/browser/index.js +5 -1
- package/dist/src/browser/keytarShim.js +56 -0
- package/dist/src/browser/policies.js +4 -0
- package/dist/src/browser/profileSync.js +141 -0
- package/dist/src/browser/projectSourcesRunner.js +2 -2
- package/dist/src/browser/reattach.js +47 -35
- package/dist/src/browser/windowsCookies.js +219 -0
- package/dist/src/cli/browserConfig.js +13 -4
- package/dist/src/cli/browserDefaults.js +6 -1
- package/dist/src/cli/options.js +1 -1
- package/dist/src/cli/projectSources.js +8 -1
- package/dist/src/mcp/tools/consult.js +1 -1
- package/dist/src/oracle/thinkingTime.js +6 -0
- package/dist/src/sessionManager.js +90 -9
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +6 -6
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -83,19 +83,78 @@ function matchesThinkingStatusLabel(trimmed) {
|
|
|
83
83
|
return true;
|
|
84
84
|
return trimmed.startsWith("pro thinking") && trimmed.length <= 40;
|
|
85
85
|
}
|
|
86
|
+
// Single source of truth for the "Answer now" placeholder test.
|
|
87
|
+
//
|
|
88
|
+
// This function is ALSO injected verbatim into page expressions via
|
|
89
|
+
// buildAnswerNowPlaceholderPredicateJs (it is stringified with
|
|
90
|
+
// Function.prototype.toString), so it MUST stay closure-free: no imports, no module
|
|
91
|
+
// constants, no helpers. Anything it references must be declared inside its own body
|
|
92
|
+
// or it will throw a ReferenceError inside the renderer.
|
|
93
|
+
//
|
|
94
|
+
// Learned the hard way: ChatGPT's Pro UI keeps the "Answer now" skip-ahead control
|
|
95
|
+
// mounted for the whole reasoning phase, so a bare `text.includes('answer now')` test
|
|
96
|
+
// matched real answers that merely ended with that chrome and discarded them wholesale
|
|
97
|
+
// at every extraction layer. A placeholder is short UI furniture, so this matches the
|
|
98
|
+
// WHOLE trimmed string against known chrome labels behind a length cap, the way the
|
|
99
|
+
// sibling predicates in this file already do.
|
|
100
|
+
//
|
|
101
|
+
// The cap is 60, taken from isActiveLabel in ./thinkingStatus.ts rather than the 40 used
|
|
102
|
+
// by matchesThinkingStatusLabel above: 40 bounds a single status label, while the longest
|
|
103
|
+
// genuine placeholder here is the composite
|
|
104
|
+
// "chatgpt said: file upload request pro thinking answer now" (57 chars), which a 40 cap
|
|
105
|
+
// would wrongly let through.
|
|
86
106
|
export function isAnswerNowPlaceholderText(value) {
|
|
87
|
-
|
|
107
|
+
let raw = "";
|
|
108
|
+
if (typeof value === "string") {
|
|
109
|
+
raw = value;
|
|
110
|
+
}
|
|
111
|
+
else if (value && typeof value === "object" && "text" in value) {
|
|
112
|
+
const candidate = value.text;
|
|
113
|
+
if (typeof candidate === "string")
|
|
114
|
+
raw = candidate;
|
|
115
|
+
}
|
|
116
|
+
const text = raw.toLowerCase().replace(/\s+/g, " ").trim();
|
|
88
117
|
if (!text)
|
|
89
118
|
return false;
|
|
90
|
-
// Learned: "Pro thinking" shows a placeholder turn that contains "Answer now".
|
|
91
|
-
// That is not the final answer and must be ignored in browser automation.
|
|
92
119
|
if (text === "chatgpt said:" || text === "chatgpt said")
|
|
93
120
|
return true;
|
|
94
|
-
if (text.
|
|
95
|
-
|
|
96
|
-
|
|
121
|
+
if (text.length > 60)
|
|
122
|
+
return false;
|
|
123
|
+
// Whole-string match: consume the text left to right, longest chrome label first.
|
|
124
|
+
// Any residue means real content is present, so this is an answer, not a placeholder.
|
|
125
|
+
const chromeLabels = [
|
|
126
|
+
"chatgpt said:",
|
|
127
|
+
"chatgpt said",
|
|
128
|
+
"file upload request",
|
|
129
|
+
"pro thinking",
|
|
130
|
+
"answer now",
|
|
131
|
+
];
|
|
132
|
+
let rest = text;
|
|
133
|
+
let sawOwner = false; // "pro thinking" / "chatgpt said"
|
|
134
|
+
let sawGate = false; // "answer now" / "file upload request"
|
|
135
|
+
while (rest.length > 0) {
|
|
136
|
+
let matched = "";
|
|
137
|
+
for (const label of chromeLabels) {
|
|
138
|
+
if (label.length > matched.length && rest.startsWith(label))
|
|
139
|
+
matched = label;
|
|
140
|
+
}
|
|
141
|
+
if (!matched)
|
|
142
|
+
return false;
|
|
143
|
+
if (matched === "answer now" || matched === "file upload request")
|
|
144
|
+
sawGate = true;
|
|
145
|
+
else
|
|
146
|
+
sawOwner = true;
|
|
147
|
+
rest = rest.slice(matched.length).replace(/^[\s:.,;|\u00b7\u2022-]+/, "");
|
|
97
148
|
}
|
|
98
|
-
return
|
|
149
|
+
return sawGate && sawOwner;
|
|
150
|
+
}
|
|
151
|
+
// Exported: the two page expressions below and the tests all consume this one builder,
|
|
152
|
+
// so the predicate cannot drift between the node side and the renderer.
|
|
153
|
+
export function buildAnswerNowPlaceholderPredicateJs(fnName) {
|
|
154
|
+
// Inject the node-side definition itself so a correction can never land in only one
|
|
155
|
+
// copy. `const x = function name(){}` is a valid expression, so the exported
|
|
156
|
+
// declaration stringifies straight into the page expression.
|
|
157
|
+
return `const ${fnName} = ${isAnswerNowPlaceholderText.toString()};`;
|
|
99
158
|
}
|
|
100
159
|
function buildActiveThinkingStatusPredicateJs(fnName) {
|
|
101
160
|
const labelsLiteral = JSON.stringify(THINKING_STATUS_LABELS);
|
|
@@ -288,6 +347,9 @@ export function buildAssistantExtractorForTest(name) {
|
|
|
288
347
|
export function buildAssistantSnapshotExpressionForTest(minTurnIndex, expectedConversationId) {
|
|
289
348
|
return buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId);
|
|
290
349
|
}
|
|
350
|
+
export function buildResponseObserverExpressionForTest(timeoutMs, minTurnIndex, expectedConversationId) {
|
|
351
|
+
return buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConversationId);
|
|
352
|
+
}
|
|
291
353
|
export function buildConversationDebugExpressionForTest() {
|
|
292
354
|
return buildConversationDebugExpression();
|
|
293
355
|
}
|
|
@@ -629,14 +691,7 @@ function buildAssistantSnapshotExpression(minTurnIndex, expectedConversationId)
|
|
|
629
691
|
// Learned: the default turn DOM misses project view; keep a fallback extractor.
|
|
630
692
|
${buildAssistantExtractor("extractAssistantTurn")}
|
|
631
693
|
const extracted = extractAssistantTurn();
|
|
632
|
-
|
|
633
|
-
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
634
|
-
if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
|
|
635
|
-
if (normalized.includes('file upload request') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
636
|
-
return true;
|
|
637
|
-
}
|
|
638
|
-
return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
|
|
639
|
-
};
|
|
694
|
+
${buildAnswerNowPlaceholderPredicateJs("isPlaceholder")}
|
|
640
695
|
${buildActiveThinkingStatusPredicateJs("isActiveThinkingStatus")}
|
|
641
696
|
if (
|
|
642
697
|
extracted &&
|
|
@@ -682,14 +737,7 @@ function buildResponseObserverExpression(timeoutMs, minTurnIndex, expectedConver
|
|
|
682
737
|
const currentId = currentConversationId();
|
|
683
738
|
return !currentId || currentId === EXPECTED_CONVERSATION_ID;
|
|
684
739
|
};
|
|
685
|
-
|
|
686
|
-
const normalized = String(snapshot?.text ?? '').toLowerCase().trim();
|
|
687
|
-
if (normalized === 'chatgpt said:' || normalized === 'chatgpt said') return true;
|
|
688
|
-
if (normalized.includes('file upload request') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'))) {
|
|
689
|
-
return true;
|
|
690
|
-
}
|
|
691
|
-
return normalized.includes('answer now') && (normalized.includes('pro thinking') || normalized.includes('chatgpt said'));
|
|
692
|
-
};
|
|
740
|
+
${buildAnswerNowPlaceholderPredicateJs("isAnswerNowPlaceholder")}
|
|
693
741
|
${buildActiveThinkingStatusPredicateJs("isActiveThinkingStatus")}
|
|
694
742
|
${buildThinkingActivePredicateJs("isThinkingActiveNow")}
|
|
695
743
|
|
|
@@ -224,10 +224,24 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
224
224
|
);
|
|
225
225
|
|
|
226
226
|
const isVisibleElement = (node) => {
|
|
227
|
+
if (typeof HTMLElement === 'undefined') return false;
|
|
227
228
|
if (!(node instanceof HTMLElement)) return false;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
try {
|
|
230
|
+
const rect = node.getBoundingClientRect?.();
|
|
231
|
+
const style = window.getComputedStyle?.(node);
|
|
232
|
+
if (!rect) return true;
|
|
233
|
+
return (
|
|
234
|
+
rect.width > 0 &&
|
|
235
|
+
rect.height > 0 &&
|
|
236
|
+
style?.display !== 'none' &&
|
|
237
|
+
style?.visibility !== 'hidden'
|
|
238
|
+
);
|
|
239
|
+
} catch {
|
|
240
|
+
// DOM test doubles and older Chromium nodes can omit one of these APIs.
|
|
241
|
+
// Treat them as visible; real hidden picker rows still report a zero rect
|
|
242
|
+
// or display:none and are filtered above.
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
231
245
|
};
|
|
232
246
|
const looksLikeModelPill = (node) => {
|
|
233
247
|
if (!(node instanceof HTMLElement) || !node.matches('button.__composer-pill')) return false;
|
|
@@ -383,6 +397,103 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
383
397
|
}
|
|
384
398
|
return true;
|
|
385
399
|
};
|
|
400
|
+
|
|
401
|
+
// ---------- Unified Intelligence picker: Advanced -> Model submenu ----------
|
|
402
|
+
// The slider picker exposes only the current effort in the composer pill. Its
|
|
403
|
+
// model versions live behind Advanced -> Model, next to an identically-shaped
|
|
404
|
+
// Effort submenu. Identify the Model opener positively so a Pro effort label can
|
|
405
|
+
// never be mistaken for a model target.
|
|
406
|
+
const ADVANCED_VIEW_SELECTOR = '[data-testid="composer-model-picker-slider-advanced-view"]';
|
|
407
|
+
const INTELLIGENCE_PICKER_SELECTOR = '[data-testid="composer-intelligence-picker-content"]';
|
|
408
|
+
const SUBMENU_OPENER_SELECTOR = '[role="menuitem"][aria-haspopup="menu"]';
|
|
409
|
+
const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', 'avanzado', 'avancado', 'avance'];
|
|
410
|
+
const MODEL_WORDS = ['model', 'modell', '模型', 'modelo', 'modello', 'modele'];
|
|
411
|
+
const EFFORT_WORDS = [
|
|
412
|
+
'effort', 'aufwand', '强度', '努力',
|
|
413
|
+
'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
|
|
414
|
+
];
|
|
415
|
+
const pickerNodeLabel = (node) => {
|
|
416
|
+
const value =
|
|
417
|
+
(node?.getAttribute?.('aria-label') ?? '') + ' ' + (node?.textContent ?? '');
|
|
418
|
+
try {
|
|
419
|
+
return value
|
|
420
|
+
.toLowerCase()
|
|
421
|
+
.normalize('NFD')
|
|
422
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
423
|
+
.replace(/\\s+/g, ' ')
|
|
424
|
+
.trim();
|
|
425
|
+
} catch {
|
|
426
|
+
return value.toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const containsPickerWord = (label, words) =>
|
|
430
|
+
words.some((word) => label.includes(word));
|
|
431
|
+
const isSubmenuOpener = (node) =>
|
|
432
|
+
node?.getAttribute?.('role') === 'menuitem' &&
|
|
433
|
+
node?.getAttribute?.('aria-haspopup') === 'menu';
|
|
434
|
+
const isUnifiedPickerMenu = (menu) =>
|
|
435
|
+
Boolean(
|
|
436
|
+
menu?.getAttribute?.('data-testid') === 'composer-intelligence-picker-content' ||
|
|
437
|
+
menu?.querySelector?.(INTELLIGENCE_PICKER_SELECTOR) ||
|
|
438
|
+
menu?.querySelector?.(ADVANCED_VIEW_SELECTOR),
|
|
439
|
+
);
|
|
440
|
+
const findUnifiedPickerMenu = () =>
|
|
441
|
+
Array.from(document.querySelectorAll(${menuContainerLiteral})).find(isUnifiedPickerMenu) ??
|
|
442
|
+
null;
|
|
443
|
+
const findAdvancedToggle = (menu) => {
|
|
444
|
+
for (const item of (menu || document).querySelectorAll('[role="menuitem"]')) {
|
|
445
|
+
if (!isVisibleElement(item)) continue;
|
|
446
|
+
if (containsPickerWord(pickerNodeLabel(item), ADVANCED_WORDS)) return item;
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
};
|
|
450
|
+
const findModelSubmenuOpener = (menu) => {
|
|
451
|
+
const scope = menu?.querySelector?.(ADVANCED_VIEW_SELECTOR) || menu || document;
|
|
452
|
+
for (const item of scope.querySelectorAll(SUBMENU_OPENER_SELECTOR)) {
|
|
453
|
+
if (!isVisibleElement(item) || !isSubmenuOpener(item)) continue;
|
|
454
|
+
const label = pickerNodeLabel(item);
|
|
455
|
+
if (
|
|
456
|
+
containsPickerWord(label, MODEL_WORDS) &&
|
|
457
|
+
!containsPickerWord(label, EFFORT_WORDS)
|
|
458
|
+
) {
|
|
459
|
+
return item;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return null;
|
|
463
|
+
};
|
|
464
|
+
const advancedModelSignalMatchesTarget = (menu = null) => {
|
|
465
|
+
const parentMenu = menu || findUnifiedPickerMenu();
|
|
466
|
+
const opener = findModelSubmenuOpener(parentMenu);
|
|
467
|
+
if (!opener) return false;
|
|
468
|
+
const label = normalizeText(pickerNodeLabel(opener));
|
|
469
|
+
const version = versionFromLabel(label);
|
|
470
|
+
if (desiredVersion && version !== desiredVersion) return false;
|
|
471
|
+
if (desiredModelVariant && !label.split(' ').includes(desiredModelVariant)) return false;
|
|
472
|
+
if (wantsPro) return labelHasProWord(label) && !labelHasLegacyProVersion(label);
|
|
473
|
+
if (wantsInstant) return label.includes('instant');
|
|
474
|
+
if (wantsThinking) return Boolean(desiredVersion) && !labelHasProWord(label);
|
|
475
|
+
if (desiredVersion) return true;
|
|
476
|
+
return normalizedTokens.some((token) => token && label.includes(token));
|
|
477
|
+
};
|
|
478
|
+
const getAdvancedModelLabel = () => {
|
|
479
|
+
const opener = findModelSubmenuOpener(findUnifiedPickerMenu());
|
|
480
|
+
if (!opener) return '';
|
|
481
|
+
const raw = (opener.textContent ?? '').trim();
|
|
482
|
+
const normalized = normalizeText(pickerNodeLabel(opener));
|
|
483
|
+
const version = versionFromLabel(normalized);
|
|
484
|
+
if (!version) return raw;
|
|
485
|
+
const [major, minor] = version.split('-');
|
|
486
|
+
const suffix = normalized.split(' ').includes('sol') ? ' Sol' : '';
|
|
487
|
+
return 'GPT-' + major + '.' + minor + suffix;
|
|
488
|
+
};
|
|
489
|
+
const isLegacyFlatIntelligencePickerOpen = () => {
|
|
490
|
+
const menu = findUnifiedPickerMenu();
|
|
491
|
+
return Boolean(
|
|
492
|
+
menu &&
|
|
493
|
+
!menu.querySelector?.(ADVANCED_VIEW_SELECTOR) &&
|
|
494
|
+
!findAdvancedToggle(menu),
|
|
495
|
+
);
|
|
496
|
+
};
|
|
386
497
|
const getResolvedLabel = (observedOptionLabel = '') => {
|
|
387
498
|
if (configuredSelectionMatchesTarget()) {
|
|
388
499
|
const variant = getConfiguredVariantLabel();
|
|
@@ -390,6 +501,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
390
501
|
if (desiredModelVariant === 'sol' && labelHasProWord(normalizeText(variant))) return version;
|
|
391
502
|
return [variant, version].filter(Boolean).join(' ');
|
|
392
503
|
}
|
|
504
|
+
if (advancedModelSignalMatchesTarget()) {
|
|
505
|
+
return getAdvancedModelLabel();
|
|
506
|
+
}
|
|
393
507
|
const composerLabel = getComposerModelLabel();
|
|
394
508
|
const normalizedComposerLabel = normalizeText(composerLabel);
|
|
395
509
|
if (
|
|
@@ -448,8 +562,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
448
562
|
desiredVersion === '5-5' &&
|
|
449
563
|
!hasProComposerPill() &&
|
|
450
564
|
isThinkingEffortLabel(normalizedLabel) &&
|
|
451
|
-
(
|
|
452
|
-
|
|
565
|
+
(isTargetGpt55VisibleAlias(readComposerModelSignal()) ||
|
|
566
|
+
(isNonProIntelligenceThinkingLabel(normalizedLabel) &&
|
|
567
|
+
isLegacyFlatIntelligencePickerOpen()))
|
|
453
568
|
) {
|
|
454
569
|
return true;
|
|
455
570
|
}
|
|
@@ -521,6 +636,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
521
636
|
return COMPOSER_SIGNAL_INCLUDES.some((token) => token && signal.includes(token));
|
|
522
637
|
};
|
|
523
638
|
const activeSelectionMatchesTarget = () => {
|
|
639
|
+
if (advancedModelSignalMatchesTarget()) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
524
642
|
if (buttonMatchesTarget()) {
|
|
525
643
|
return true;
|
|
526
644
|
}
|
|
@@ -952,6 +1070,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
952
1070
|
for (const menu of menus) {
|
|
953
1071
|
const buttons = Array.from(menu.querySelectorAll(${menuItemLiteral}));
|
|
954
1072
|
for (const option of buttons) {
|
|
1073
|
+
if (!isVisibleElement(option)) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
955
1076
|
if (isNestedEffortControl(option, menu)) {
|
|
956
1077
|
continue;
|
|
957
1078
|
}
|
|
@@ -1038,6 +1159,33 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
1038
1159
|
dispatchHoverSequence(node);
|
|
1039
1160
|
dispatchClickSequence(node);
|
|
1040
1161
|
};
|
|
1162
|
+
const descendIntoAdvancedModelPicker = () => {
|
|
1163
|
+
const parentMenu = findUnifiedPickerMenu();
|
|
1164
|
+
if (!parentMenu) return 'unavailable';
|
|
1165
|
+
|
|
1166
|
+
const opener = findModelSubmenuOpener(parentMenu);
|
|
1167
|
+
if (opener) {
|
|
1168
|
+
if (advancedModelSignalMatchesTarget(parentMenu)) {
|
|
1169
|
+
return 'already-selected';
|
|
1170
|
+
}
|
|
1171
|
+
const key = submenuKey(
|
|
1172
|
+
normalizeText(opener.textContent ?? ''),
|
|
1173
|
+
opener.getAttribute?.('data-testid') ?? '',
|
|
1174
|
+
);
|
|
1175
|
+
openedSubmenuKeys.add(key);
|
|
1176
|
+
if (opener.getAttribute?.('aria-expanded') !== 'true') {
|
|
1177
|
+
openSubmenuOption(opener);
|
|
1178
|
+
}
|
|
1179
|
+
return 'opened';
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const advancedToggle = findAdvancedToggle(parentMenu);
|
|
1183
|
+
if (!advancedToggle) return 'unavailable';
|
|
1184
|
+
if (advancedToggle.getAttribute?.('aria-expanded') !== 'true') {
|
|
1185
|
+
dispatchClickSequence(advancedToggle);
|
|
1186
|
+
}
|
|
1187
|
+
return 'expanded';
|
|
1188
|
+
};
|
|
1041
1189
|
|
|
1042
1190
|
return new Promise((resolve) => {
|
|
1043
1191
|
const start = performance.now();
|
|
@@ -1124,6 +1272,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
1124
1272
|
});
|
|
1125
1273
|
return;
|
|
1126
1274
|
}
|
|
1275
|
+
const advancedState = descendIntoAdvancedModelPicker();
|
|
1276
|
+
if (advancedState === 'already-selected') {
|
|
1277
|
+
const resolvedLabel = getResolvedLabel();
|
|
1278
|
+
closeMenu();
|
|
1279
|
+
resolve({ status: 'already-selected', label: resolvedLabel });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
if (advancedState === 'expanded' || advancedState === 'opened') {
|
|
1283
|
+
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1127
1286
|
if (performance.now() - start > MAX_WAIT_MS) {
|
|
1128
1287
|
resolve({
|
|
1129
1288
|
status: 'option-not-found',
|
|
@@ -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,18 +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
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
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}.`));
|
|
61
70
|
return;
|
|
62
71
|
}
|
|
63
72
|
default: {
|
|
64
73
|
await logDomFailure(Runtime, logger, "thinking-time-unknown");
|
|
65
74
|
logPickerDiagnostic(result, logger);
|
|
66
75
|
if (strictProEffort) {
|
|
67
|
-
|
|
76
|
+
const target = level === "pro" ? "Pro" : "Pro Extended";
|
|
77
|
+
throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed ${target}.`);
|
|
68
78
|
}
|
|
69
79
|
logger(formatBrowserThinkingLog(`unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`));
|
|
70
80
|
return;
|
|
@@ -74,7 +84,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
74
84
|
/**
|
|
75
85
|
* Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
|
|
76
86
|
* Safe by default: if the pill/menu/option isn't present, we continue without throwing.
|
|
77
|
-
* @param level - The thinking time intensity: 'light', 'standard', 'extended', 'extra-high', or 'heavy'
|
|
87
|
+
* @param level - The thinking time intensity: 'light', 'standard', 'extended', 'extra-high', 'pro', or 'heavy'
|
|
78
88
|
*/
|
|
79
89
|
export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desiredModel) {
|
|
80
90
|
try {
|
|
@@ -137,15 +147,20 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
137
147
|
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
138
148
|
const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
|
|
139
149
|
|
|
140
|
-
// Multilingual matchers: English level token + observed German/Chinese variants.
|
|
150
|
+
// Multilingual matchers: English level token + observed German/Japanese/Chinese variants.
|
|
141
151
|
const LEVEL_TOKENS = {
|
|
142
|
-
light: ['light', 'instant', 'sofort', 'leicht', '轻', '极速'],
|
|
143
|
-
standard: ['standard', 'medium', 'mittel', '标准', '中'],
|
|
144
|
-
extended: ['extended', 'high', 'hoch', 'erweitert', '扩展', '深度', '加强', '高'],
|
|
145
|
-
'extra-high': ['extra high', 'sehr hoch', '极高'],
|
|
152
|
+
light: ['light', 'instant', 'sofort', 'leicht', '最速', '轻', '极速'],
|
|
153
|
+
standard: ['standard', 'medium', 'mittel', '中程度', '标准', '中'],
|
|
154
|
+
extended: ['extended', 'high', 'hoch', 'erweitert', '高い', '扩展', '深度', '加强', '高'],
|
|
155
|
+
'extra-high': ['extra high', 'sehr hoch', '非常に高い', '极高'],
|
|
146
156
|
heavy: ['heavy', 'schwer', '重度', '加重'],
|
|
147
157
|
};
|
|
148
|
-
|
|
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];
|
|
149
164
|
|
|
150
165
|
const INITIAL_WAIT_MS = 150;
|
|
151
166
|
const STEP_WAIT_MS = 200;
|
|
@@ -156,7 +171,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
156
171
|
const INTELLIGENCE_WAIT_MS = 2500;
|
|
157
172
|
|
|
158
173
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
159
|
-
// Keep CJK characters
|
|
174
|
+
// Keep CJK characters, including Japanese kana, so localized labels survive
|
|
175
|
+
// normalization before being matched against LEVEL_TOKENS and picker controls.
|
|
160
176
|
const normalize = (value) => (value || '')
|
|
161
177
|
// Compose first so NFD umlauts fold too, then map them onto ASCII before
|
|
162
178
|
// the strip below would drop them (and split the token in half).
|
|
@@ -166,7 +182,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
166
182
|
.replace(/ö/g, 'o')
|
|
167
183
|
.replace(/ü/g, 'u')
|
|
168
184
|
.replace(/ß/g, 'ss')
|
|
169
|
-
.replace(/[^a-z0-9\\u4e00-\\
|
|
185
|
+
.replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9fff]+/g, ' ')
|
|
170
186
|
.replace(/\\s+/g, ' ')
|
|
171
187
|
.trim();
|
|
172
188
|
const hasToken = (text, token) => normalize(text).split(' ').includes(token);
|
|
@@ -195,6 +211,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
195
211
|
if (token === 'extra high') return hasPhrase(t, 'extra high');
|
|
196
212
|
if (token === 'hoch') return hasPhrase(t, 'hoch') && !hasPhrase(t, 'sehr hoch');
|
|
197
213
|
if (token === 'sehr hoch') return hasPhrase(t, 'sehr hoch');
|
|
214
|
+
if (token === '高い' || token === '非常に高い') {
|
|
215
|
+
return t === token || hasToken(t, token);
|
|
216
|
+
}
|
|
198
217
|
if (token === '极速') {
|
|
199
218
|
const suffix = t.slice(token.length);
|
|
200
219
|
return t === token || hasToken(t, token) || /^[0-9]/.test(suffix);
|
|
@@ -372,7 +391,20 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
372
391
|
diagnostic: collectPickerDiagnostic(),
|
|
373
392
|
});
|
|
374
393
|
const findOptionInMenu = (menu, modelKindOverride = null) => {
|
|
375
|
-
|
|
394
|
+
// Container controls reveal other controls; they are not tiers you can pick.
|
|
395
|
+
// Two shapes exist and both can collide with a tier label: a submenu opener
|
|
396
|
+
// ("ModelGPT-5.6 Pro" would satisfy a Pro request) and a disclosure toggle
|
|
397
|
+
// (German "Erweitert" is literally one of the extended tokens, so the
|
|
398
|
+
// Advanced toggle would satisfy an extended request and be clicked in place
|
|
399
|
+
// of the tier). Detect them structurally rather than by label: a real tier row
|
|
400
|
+
// carries a checked state, while a container carries expansion state.
|
|
401
|
+
const isContainerControl = (node) =>
|
|
402
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' ||
|
|
403
|
+
(node?.getAttribute?.('aria-expanded') !== null &&
|
|
404
|
+
node?.getAttribute?.('aria-checked') === null);
|
|
405
|
+
const items = Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR)).filter(
|
|
406
|
+
(item) => !isContainerControl(item),
|
|
407
|
+
);
|
|
376
408
|
const modelKind = modelKindOverride || effectiveTargetModelKind();
|
|
377
409
|
if (modelKind === 'pro') {
|
|
378
410
|
// GPT-5.6's unified Intelligence picker exposes Pro as the highest
|
|
@@ -414,7 +446,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
414
446
|
const itemText = normalize(
|
|
415
447
|
(item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
|
|
416
448
|
);
|
|
417
|
-
|
|
449
|
+
// Pro rows are skipped for non-Pro targets so "High" never resolves to
|
|
450
|
+
// "Pro". Two things lift this: an explicit TARGET_LEVEL of 'pro', and a
|
|
451
|
+
// legacy Pro-model menu (modelKind === 'pro'), whose rows are all Pro
|
|
452
|
+
// variants so excluding them would leave nothing to match.
|
|
453
|
+
if (TARGET_LEVEL !== 'pro' && modelKind !== 'pro' && hasToken(itemText, 'pro')) {
|
|
418
454
|
continue;
|
|
419
455
|
}
|
|
420
456
|
if (
|
|
@@ -644,6 +680,122 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
644
680
|
return null;
|
|
645
681
|
};
|
|
646
682
|
|
|
683
|
+
// ---------- Unified Intelligence picker: Advanced -> Effort submenu ----------
|
|
684
|
+
// A newer ChatGPT layout replaces the flat effort rows with a "power" slider
|
|
685
|
+
// (simple view) plus an "Advanced" view holding two submenu openers: Model and
|
|
686
|
+
// Effort. The tier rows only exist inside the Effort submenu, so the flat scan
|
|
687
|
+
// of the top-level menu finds nothing and we must expand and descend.
|
|
688
|
+
const ADVANCED_VIEW_SELECTOR = '[data-testid="composer-model-picker-slider-advanced-view"]';
|
|
689
|
+
const SUBMENU_OPENER_SELECTOR = '[role="menuitem"][aria-haspopup="menu"]';
|
|
690
|
+
const nodeLabel = (node) =>
|
|
691
|
+
normalize((node?.getAttribute?.('aria-label') ?? '') + ' ' + (node?.textContent ?? ''));
|
|
692
|
+
// Row labels in this menu concatenate without separators ("EffortHigh"), so
|
|
693
|
+
// token matching cannot be used here — substring is deliberate, as in
|
|
694
|
+
// countEffortLevels above.
|
|
695
|
+
const ADVANCED_WORDS = [
|
|
696
|
+
'advanced', 'erweitert', '高级', '詳細設定', '詳細表示',
|
|
697
|
+
'avanzado', 'avancado', 'avance',
|
|
698
|
+
];
|
|
699
|
+
const EFFORT_WORDS = [
|
|
700
|
+
'effort', 'aufwand', '强度', '努力', '推論レベル',
|
|
701
|
+
'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
|
|
702
|
+
];
|
|
703
|
+
const containsAny = (label, words) => words.some((word) => label.includes(word));
|
|
704
|
+
const findAdvancedToggle = (menu) => {
|
|
705
|
+
for (const item of (menu || document).querySelectorAll('[role="menuitem"]')) {
|
|
706
|
+
if (!isVisible(item)) continue;
|
|
707
|
+
if (containsAny(nodeLabel(item), ADVANCED_WORDS)) return item;
|
|
708
|
+
}
|
|
709
|
+
return null;
|
|
710
|
+
};
|
|
711
|
+
// Verify the shape rather than trusting the selector: a tier row must never be
|
|
712
|
+
// mistaken for the opener, or hovering it would silently change the effort.
|
|
713
|
+
const isSubmenuOpener = (node) =>
|
|
714
|
+
node?.getAttribute?.('aria-haspopup') === 'menu' &&
|
|
715
|
+
(node?.getAttribute?.('role') ?? '') === 'menuitem';
|
|
716
|
+
// The Effort opener's label is the word "Effort" plus the tier it currently sits
|
|
717
|
+
// on ("EffortHigh"). Positive identification only: the sibling Model opener has
|
|
718
|
+
// the identical shape and can read "ModelGPT-5.6 Pro", so guessing from tier
|
|
719
|
+
// words would pick it and then click model rows as if they were efforts. An
|
|
720
|
+
// unrecognised language yields no opener, and the caller fails instead of
|
|
721
|
+
// gambling on the wrong control.
|
|
722
|
+
const findEffortSubmenuOpener = (menu) => {
|
|
723
|
+
const scope = menu?.querySelector?.(ADVANCED_VIEW_SELECTOR) || menu || document;
|
|
724
|
+
for (const item of scope.querySelectorAll(SUBMENU_OPENER_SELECTOR)) {
|
|
725
|
+
if (!isVisible(item) || !isSubmenuOpener(item)) continue;
|
|
726
|
+
if (containsAny(nodeLabel(item), EFFORT_WORDS)) return item;
|
|
727
|
+
}
|
|
728
|
+
return null;
|
|
729
|
+
};
|
|
730
|
+
// countEffortLevels reads aggregate menu text, where one "Extra High" row scores
|
|
731
|
+
// twice (once as "high", once as "extra high"). Count distinct levels across
|
|
732
|
+
// distinct rows instead, so ">= 2" really means two selectable tiers.
|
|
733
|
+
const countDistinctTierRows = (menu) => {
|
|
734
|
+
if (!menu) return 0;
|
|
735
|
+
const matched = new Set();
|
|
736
|
+
for (const row of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
|
|
737
|
+
if (isSubmenuOpener(row)) continue;
|
|
738
|
+
const text = nodeLabel(row);
|
|
739
|
+
for (const [level, tokens] of Object.entries(LEVEL_TOKENS)) {
|
|
740
|
+
if (matchesTokens(text, tokens)) matched.add(level);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return matched.size;
|
|
744
|
+
};
|
|
745
|
+
const resolveSubmenuFor = (opener, parentMenu) => {
|
|
746
|
+
const id = opener?.getAttribute?.('aria-controls');
|
|
747
|
+
if (id) {
|
|
748
|
+
const node = document.getElementById?.(id);
|
|
749
|
+
if (isVisible(node) && countDistinctTierRows(node) >= 2) return node;
|
|
750
|
+
}
|
|
751
|
+
let best = null;
|
|
752
|
+
for (const menu of document.querySelectorAll(MENU_CONTAINER_SELECTOR)) {
|
|
753
|
+
if (menu === parentMenu || menu.contains?.(opener) || !isVisible(menu)) continue;
|
|
754
|
+
const hits = countDistinctTierRows(menu);
|
|
755
|
+
if (hits >= 2 && (!best || hits > best.hits)) best = { menu, hits };
|
|
756
|
+
}
|
|
757
|
+
return best?.menu ?? null;
|
|
758
|
+
};
|
|
759
|
+
const selectEffortFromAdvancedSubmenu = async (parentMenu, modelKindOverride = null) => {
|
|
760
|
+
if (!parentMenu) return null;
|
|
761
|
+
// React can mount the advanced view well after the click under load, so poll
|
|
762
|
+
// for the opener to the same deadline the submenu gets instead of assuming it
|
|
763
|
+
// rendered within one STEP_WAIT_MS. Re-expand if the toggle collapses again.
|
|
764
|
+
let opener = null;
|
|
765
|
+
const openerDeadline = performance.now() + MAX_WAIT_MS;
|
|
766
|
+
while (!opener && performance.now() < openerDeadline) {
|
|
767
|
+
const toggle = findAdvancedToggle(parentMenu);
|
|
768
|
+
if (toggle && toggle.getAttribute?.('aria-expanded') === 'false') {
|
|
769
|
+
dispatchClickSequence(toggle);
|
|
770
|
+
await sleep(STEP_WAIT_MS);
|
|
771
|
+
}
|
|
772
|
+
opener = findEffortSubmenuOpener(parentMenu);
|
|
773
|
+
if (opener) break;
|
|
774
|
+
await sleep(100);
|
|
775
|
+
}
|
|
776
|
+
if (!opener) return null;
|
|
777
|
+
dispatchHoverSequence(opener);
|
|
778
|
+
if (opener.getAttribute?.('aria-expanded') !== 'true') {
|
|
779
|
+
dispatchClickSequence(opener);
|
|
780
|
+
}
|
|
781
|
+
const deadline = performance.now() + MAX_WAIT_MS;
|
|
782
|
+
while (performance.now() < deadline) {
|
|
783
|
+
const submenu = resolveSubmenuFor(opener, parentMenu);
|
|
784
|
+
if (submenu) {
|
|
785
|
+
return selectAndVerify(
|
|
786
|
+
opener,
|
|
787
|
+
() => {
|
|
788
|
+
const current = resolveSubmenuFor(opener, parentMenu);
|
|
789
|
+
return current ? findOptionInMenu(current, modelKindOverride) : null;
|
|
790
|
+
},
|
|
791
|
+
modelKindOverride,
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
await sleep(100);
|
|
795
|
+
}
|
|
796
|
+
return null;
|
|
797
|
+
};
|
|
798
|
+
|
|
647
799
|
// Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
|
|
648
800
|
// controlled menu contains the effort levels. Prefer this ownership boundary
|
|
649
801
|
// before probing older model-picker layouts.
|
|
@@ -740,9 +892,26 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
740
892
|
}
|
|
741
893
|
if (composerEffortPill) {
|
|
742
894
|
if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
|
|
895
|
+
// In the unified Intelligence picker the composer pill shows the current
|
|
896
|
+
// EFFORT ("Pro", "High"), not the model. Reading a Pro *model* out of it would
|
|
897
|
+
// lift the Pro-row exclusion in findOptionInMenu and let a lower-tier request
|
|
898
|
+
// settle on Pro. Only a pill naming a tier and nothing else qualifies: legacy
|
|
899
|
+
// pills read "Pro Extended" (model + effort) and must keep naming their model,
|
|
900
|
+
// or a Pro Extended user asking for extended would be moved down to High.
|
|
901
|
+
const pillLabel = normalize(
|
|
902
|
+
(composerEffortPill.getAttribute?.('aria-label') ?? '') +
|
|
903
|
+
' ' +
|
|
904
|
+
(composerEffortPill.textContent ?? ''),
|
|
905
|
+
);
|
|
906
|
+
const pillIsBareEffortTier = Object.values(TARGET_LEVEL_TOKENS).some((tokens) =>
|
|
907
|
+
tokens.some((token) => normalize(token) === pillLabel),
|
|
908
|
+
);
|
|
909
|
+
const pillNamesEffortNotModel =
|
|
910
|
+
TARGET_IS_GPT56_MODEL ||
|
|
911
|
+
(pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
|
|
743
912
|
const composerModelKind =
|
|
744
913
|
TARGET_MODEL_KIND ||
|
|
745
|
-
(
|
|
914
|
+
(pillNamesEffortNotModel ? 'versioned' : modelKindFromNode(composerEffortPill));
|
|
746
915
|
if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
|
|
747
916
|
dispatchClickSequence(composerEffortPill);
|
|
748
917
|
await sleep(INITIAL_WAIT_MS);
|
|
@@ -755,6 +924,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
755
924
|
if (proEffortResult) {
|
|
756
925
|
return proEffortResult;
|
|
757
926
|
}
|
|
927
|
+
// Flat rows win when present; only descend into Advanced -> Effort when
|
|
928
|
+
// this menu has no matching tier of its own (the slider layout).
|
|
929
|
+
if (!findOptionInMenu(menu, composerModelKind)) {
|
|
930
|
+
const advancedResult = await selectEffortFromAdvancedSubmenu(menu, composerModelKind);
|
|
931
|
+
if (advancedResult) {
|
|
932
|
+
return advancedResult;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
758
935
|
return selectAndVerify(
|
|
759
936
|
composerEffortPill,
|
|
760
937
|
() => {
|