@steipete/oracle 0.15.2 → 0.16.0
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/src/browser/actions/assistantResponse.js +218 -126
- package/dist/src/browser/actions/modelSelection.js +133 -22
- package/dist/src/browser/actions/navigation.js +235 -14
- package/dist/src/browser/actions/thinkingStatus.js +228 -0
- package/dist/src/browser/actions/thinkingTime.js +68 -11
- package/dist/src/browser/chromeLifecycle.js +29 -28
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/constants.js +5 -1
- package/dist/src/browser/controlPlan.js +2 -2
- package/dist/src/browser/index.js +56 -16
- package/dist/src/browser/liveTabs.js +113 -31
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/projectSourcesRunner.js +4 -4
- package/dist/src/browser/reattach.js +2 -2
- package/dist/src/browser/recoverConversation.js +90 -29
- package/dist/src/cli/browserConfig.js +5 -1
- package/dist/src/cli/browserTabs.js +54 -33
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/runOptions.js +6 -1
- package/dist/src/cli/sessionDisplay.js +38 -14
- package/dist/src/oracle/config.js +25 -0
- package/dist/src/oracle/geminiModels.js +2 -0
- 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 +9 -10
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -569
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -416
- package/dist/docs-site/browser-mode.html +0 -594
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -462
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -375
- package/dist/docs-site/gemini.html +0 -383
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -596
- package/dist/docs-site/mcp.html +0 -391
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -399
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -36,9 +36,10 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
36
36
|
switch (result?.status) {
|
|
37
37
|
case "already-selected":
|
|
38
38
|
case "switched": {
|
|
39
|
-
const
|
|
39
|
+
const observedLabel = result.label?.trim() || null;
|
|
40
|
+
const label = observedLabel || (strategy === "current" ? null : desiredModel);
|
|
40
41
|
if (strategy !== "current") {
|
|
41
|
-
assertResolvedModelSelection(desiredModel,
|
|
42
|
+
assertResolvedModelSelection(desiredModel, observedLabel ?? "");
|
|
42
43
|
}
|
|
43
44
|
logger(`Model picker: ${label ?? "current model (label unavailable)"}`);
|
|
44
45
|
return {
|
|
@@ -70,6 +71,18 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
70
71
|
function assertResolvedModelSelection(desiredModel, resolvedLabel) {
|
|
71
72
|
const desired = desiredModel.toLowerCase();
|
|
72
73
|
const resolved = resolvedLabel.toLowerCase();
|
|
74
|
+
const normalizedDesired = normalizeResolvedModelLabel(desired);
|
|
75
|
+
const normalizedResolved = normalizeResolvedModelLabel(resolved);
|
|
76
|
+
const wantsGpt56Sol = /(?:^| )5 6(?: |$)/.test(normalizedDesired) && normalizedDesired.split(" ").includes("sol");
|
|
77
|
+
if (wantsGpt56Sol) {
|
|
78
|
+
const resolvedTokens = normalizedResolved.split(" ");
|
|
79
|
+
if (!/(?:^| )5 6(?: |$)/.test(normalizedResolved) ||
|
|
80
|
+
!resolvedTokens.includes("sol") ||
|
|
81
|
+
resolvedTokens.includes("pro")) {
|
|
82
|
+
throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-5.6 Sol.`);
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
73
86
|
const wantsGpt55Pro = desired === "pro" ||
|
|
74
87
|
desired === "chatgpt pro" ||
|
|
75
88
|
desired === "gpt-5.5-pro" ||
|
|
@@ -152,10 +165,12 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
152
165
|
.map((token) => normalizeText(token))
|
|
153
166
|
.filter(Boolean);
|
|
154
167
|
const targetWords = normalizedTarget.split(' ').filter(Boolean);
|
|
155
|
-
const desiredVersion = normalizedTarget.includes('5
|
|
156
|
-
? '5-
|
|
168
|
+
const desiredVersion = normalizedTarget.includes('5 6')
|
|
169
|
+
? '5-6'
|
|
157
170
|
: normalizedTarget.includes('5 5')
|
|
158
171
|
? '5-5'
|
|
172
|
+
: normalizedTarget.includes('5 4')
|
|
173
|
+
? '5-4'
|
|
159
174
|
: normalizedTarget.includes('5 3')
|
|
160
175
|
? '5-3'
|
|
161
176
|
: normalizedTarget.includes('5 2')
|
|
@@ -165,6 +180,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
165
180
|
: normalizedTarget.includes('5 0')
|
|
166
181
|
? '5-0'
|
|
167
182
|
: null;
|
|
183
|
+
const desiredModelVariant = normalizedTarget.includes(' sol') ? 'sol' : null;
|
|
168
184
|
const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
|
|
169
185
|
const wantsInstant = normalizedTarget.includes('instant');
|
|
170
186
|
const wantsThinking = normalizedTarget.includes('thinking');
|
|
@@ -280,12 +296,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
280
296
|
label === 'medium' ||
|
|
281
297
|
label === 'high' ||
|
|
282
298
|
label === 'extra high' ||
|
|
299
|
+
label === 'pro' ||
|
|
283
300
|
label === 'extended' ||
|
|
284
301
|
label === 'standard' ||
|
|
285
302
|
label === 'heavy' ||
|
|
286
303
|
label === 'light';
|
|
287
304
|
const formatModelOptionLabel = (label) => {
|
|
288
305
|
const normalized = normalizeText(label ?? '');
|
|
306
|
+
if (normalized === '5 6' || normalized === 'gpt 5 6') return 'GPT-5.6';
|
|
289
307
|
if (normalized === '5 5' || normalized === 'gpt 5 5') return 'GPT-5.5';
|
|
290
308
|
if (normalized === '5 4' || normalized === 'gpt 5 4') return 'GPT-5.4';
|
|
291
309
|
if (normalized === '5 3' || normalized === 'gpt 5 3') return 'GPT-5.3';
|
|
@@ -319,17 +337,12 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
319
337
|
return 1000;
|
|
320
338
|
};
|
|
321
339
|
const versionFromLabel = (label) => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (label === '5 3' || label === 'gpt 5 3') return '5-3';
|
|
325
|
-
if (label === '5 2' || label === 'gpt 5 2') return '5-2';
|
|
326
|
-
if (label === '5 1' || label === 'gpt 5 1') return '5-1';
|
|
327
|
-
if (label === '5 0' || label === 'gpt 5 0') return '5-0';
|
|
328
|
-
if (label === '4 5' || label === 'gpt 4 5') return '4-5';
|
|
329
|
-
return null;
|
|
340
|
+
const match = normalizeText(label).match(/(?:^| )(?:gpt )?(\\d+) (\\d+)(?: |$)/);
|
|
341
|
+
return match ? match[1] + '-' + match[2] : null;
|
|
330
342
|
};
|
|
331
343
|
const versionFromTestId = (testid) => {
|
|
332
344
|
const normalized = normalizeText(testid);
|
|
345
|
+
if (normalized.includes('5 6') || normalized.includes('gpt56')) return '5-6';
|
|
333
346
|
if (normalized.includes('5 5') || normalized.includes('gpt55')) return '5-5';
|
|
334
347
|
if (normalized.includes('5 4') || normalized.includes('gpt54')) return '5-4';
|
|
335
348
|
if (normalized.includes('5 3') || normalized.includes('gpt53')) return '5-3';
|
|
@@ -355,9 +368,16 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
355
368
|
return label;
|
|
356
369
|
};
|
|
357
370
|
const configuredSelectionMatchesTarget = () => {
|
|
358
|
-
const
|
|
371
|
+
const configuredVersionLabel = normalizeText(getConfiguredVersionLabel());
|
|
372
|
+
const configuredVersion = versionFromLabel(configuredVersionLabel);
|
|
359
373
|
if (!configuredVersion || configuredVersion !== desiredVersion) return false;
|
|
360
374
|
const configuredVariant = normalizeText(getConfiguredVariantLabel());
|
|
375
|
+
if (
|
|
376
|
+
desiredModelVariant &&
|
|
377
|
+
!configuredVersionLabel.includes(desiredModelVariant) &&
|
|
378
|
+
!configuredVariant.includes(desiredModelVariant)
|
|
379
|
+
) return false;
|
|
380
|
+
if (desiredModelVariant === 'sol' && labelHasProWord(configuredVariant)) return false;
|
|
361
381
|
if (wantsPro) return labelHasProWord(configuredVariant);
|
|
362
382
|
if (wantsInstant) return configuredVariant.includes('instant');
|
|
363
383
|
if (wantsThinking) {
|
|
@@ -372,10 +392,36 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
372
392
|
return [variant, version].filter(Boolean).join(' ');
|
|
373
393
|
}
|
|
374
394
|
const composerLabel = getComposerModelLabel();
|
|
375
|
-
|
|
395
|
+
const normalizedComposerLabel = normalizeText(composerLabel);
|
|
396
|
+
if (
|
|
397
|
+
desiredVersion &&
|
|
398
|
+
desiredModelVariant &&
|
|
399
|
+
versionFromLabel(normalizedComposerLabel) === desiredVersion &&
|
|
400
|
+
normalizedComposerLabel.split(' ').includes(desiredModelVariant)
|
|
401
|
+
) {
|
|
402
|
+
return withProPillSignal(composerLabel);
|
|
403
|
+
}
|
|
376
404
|
const buttonLabel = getButtonLabel();
|
|
377
405
|
const normalizedButton = normalizeText(buttonLabel);
|
|
406
|
+
if (
|
|
407
|
+
desiredVersion &&
|
|
408
|
+
desiredModelVariant &&
|
|
409
|
+
versionFromLabel(normalizedButton) === desiredVersion &&
|
|
410
|
+
normalizedButton.split(' ').includes(desiredModelVariant)
|
|
411
|
+
) {
|
|
412
|
+
return withProPillSignal(buttonLabel);
|
|
413
|
+
}
|
|
378
414
|
const fallbackLabel = formatModelOptionLabel(fallback);
|
|
415
|
+
const normalizedFallback = normalizeText(fallbackLabel);
|
|
416
|
+
if (
|
|
417
|
+
desiredVersion &&
|
|
418
|
+
desiredModelVariant &&
|
|
419
|
+
versionFromLabel(normalizedFallback) === desiredVersion &&
|
|
420
|
+
normalizedFallback.split(' ').includes(desiredModelVariant)
|
|
421
|
+
) {
|
|
422
|
+
return fallbackLabel;
|
|
423
|
+
}
|
|
424
|
+
if (composerLabel) return withProPillSignal(composerLabel);
|
|
379
425
|
if (fallbackLabel && !wantsPro && isIntelligenceEffortLabel(normalizedButton)) {
|
|
380
426
|
return fallbackLabel;
|
|
381
427
|
}
|
|
@@ -397,6 +443,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
397
443
|
if (configuredSelectionMatchesTarget()) return true;
|
|
398
444
|
const normalizedLabel = normalizeText(getButtonLabel());
|
|
399
445
|
if (!normalizedLabel) return false;
|
|
446
|
+
if (desiredModelVariant === 'sol' && hasProComposerPill()) return false;
|
|
400
447
|
if (wantsThinking && !wantsPro && hasProComposerPill()) return false;
|
|
401
448
|
if (isTargetGpt55VisibleAlias(normalizedLabel)) return true;
|
|
402
449
|
if (
|
|
@@ -421,6 +468,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
421
468
|
return true;
|
|
422
469
|
}
|
|
423
470
|
if (desiredVersion) {
|
|
471
|
+
if (desiredVersion === '5-6' && !normalizedLabel.includes('5 6')) return false;
|
|
424
472
|
if (desiredVersion === '5-5' && !normalizedLabel.includes('5 5')) return false;
|
|
425
473
|
if (desiredVersion === '5-4' && !normalizedLabel.includes('5 4')) return false;
|
|
426
474
|
if (desiredVersion === '5-3' && !normalizedLabel.includes('5 3')) return false;
|
|
@@ -428,6 +476,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
428
476
|
if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
|
|
429
477
|
if (desiredVersion === '5-0' && !normalizedLabel.includes('5 0')) return false;
|
|
430
478
|
}
|
|
479
|
+
if (desiredModelVariant && !normalizedLabel.includes(desiredModelVariant)) return false;
|
|
431
480
|
if (wantsPro && labelHasLegacyProVersion(normalizedLabel)) return false;
|
|
432
481
|
if (wantsPro && !labelHasProWord(normalizedLabel)) return false;
|
|
433
482
|
if (wantsInstant && !normalizedLabel.includes('instant')) return false;
|
|
@@ -455,6 +504,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
455
504
|
if (!signal) {
|
|
456
505
|
return COMPOSER_SIGNAL_ALLOW_BLANK;
|
|
457
506
|
}
|
|
507
|
+
if (desiredModelVariant === 'sol' && hasProComposerPill()) {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
458
510
|
if (wantsPro && labelHasLegacyProVersion(signal)) {
|
|
459
511
|
return false;
|
|
460
512
|
}
|
|
@@ -542,6 +594,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
542
594
|
let score = 0;
|
|
543
595
|
const normalizedTestId = (testid ?? '').toLowerCase();
|
|
544
596
|
const candidateTextVersion = versionFromLabel(normalizedText);
|
|
597
|
+
const candidateIsVersionSubmenuTrigger = Boolean(
|
|
598
|
+
candidateTextVersion && node?.getAttribute?.('aria-haspopup') === 'menu'
|
|
599
|
+
);
|
|
545
600
|
const candidateIsVersionCombobox =
|
|
546
601
|
node?.getAttribute?.('role') === 'combobox' &&
|
|
547
602
|
node?.getAttribute?.('aria-labelledby') === 'model-selection-label';
|
|
@@ -553,6 +608,20 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
553
608
|
desiredVersion !== '5-5' &&
|
|
554
609
|
(normalizedTestId === 'model-configure-modal' ||
|
|
555
610
|
(candidateIsVersionCombobox && node?.getAttribute?.('aria-expanded') !== 'true'));
|
|
611
|
+
const candidateIsConfiguredVersionNavigation =
|
|
612
|
+
Boolean(getConfigurationDialog()) &&
|
|
613
|
+
candidateTextVersion === desiredVersion &&
|
|
614
|
+
(node?.getAttribute?.('role') === 'option' ||
|
|
615
|
+
node?.getAttribute?.('role') === 'menuitemradio');
|
|
616
|
+
if (
|
|
617
|
+
desiredModelVariant &&
|
|
618
|
+
!normalizedText.includes(desiredModelVariant) &&
|
|
619
|
+
!candidateIsVersionSubmenuTrigger &&
|
|
620
|
+
!candidateOpensConfiguration &&
|
|
621
|
+
!candidateIsConfiguredVersionNavigation
|
|
622
|
+
) {
|
|
623
|
+
return 0;
|
|
624
|
+
}
|
|
556
625
|
if (candidateOpensConfiguration) {
|
|
557
626
|
return 2000;
|
|
558
627
|
}
|
|
@@ -560,6 +629,12 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
560
629
|
if (normalizedTestId) {
|
|
561
630
|
if (desiredVersion) {
|
|
562
631
|
// data-testid strings have been observed with both dotted and dashed versions (e.g. gpt-5.2-pro vs gpt-5-2-pro).
|
|
632
|
+
const has56 =
|
|
633
|
+
normalizedTestId.includes('5-6') ||
|
|
634
|
+
normalizedTestId.includes('5.6') ||
|
|
635
|
+
normalizedTestId.includes('gpt-5-6') ||
|
|
636
|
+
normalizedTestId.includes('gpt-5.6') ||
|
|
637
|
+
normalizedTestId.includes('gpt56');
|
|
563
638
|
const has52 =
|
|
564
639
|
normalizedTestId.includes('5-2') ||
|
|
565
640
|
normalizedTestId.includes('5.2') ||
|
|
@@ -596,8 +671,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
596
671
|
normalizedTestId.includes('gpt-5-0') ||
|
|
597
672
|
normalizedTestId.includes('gpt-5.0') ||
|
|
598
673
|
normalizedTestId.includes('gpt50');
|
|
599
|
-
const candidateVersion =
|
|
600
|
-
? '5-
|
|
674
|
+
const candidateVersion = has56
|
|
675
|
+
? '5-6'
|
|
676
|
+
: has55
|
|
677
|
+
? '5-5'
|
|
601
678
|
: has54
|
|
602
679
|
? '5-4'
|
|
603
680
|
: has53
|
|
@@ -645,10 +722,11 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
645
722
|
const candidateClearsProForThinking =
|
|
646
723
|
wantsThinking && !wantsPro && hasActiveProPill && candidateIsNonProThinkingEffort;
|
|
647
724
|
const candidateOpensVersionSubmenu =
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
725
|
+
(desiredVersion === '5-6' && candidateIsVersionSubmenuTrigger) ||
|
|
726
|
+
(wantsThinking &&
|
|
727
|
+
desiredVersion !== '5-5' &&
|
|
728
|
+
normalizedText === 'gpt 5 5' &&
|
|
729
|
+
!normalizedTestId.includes('pro'));
|
|
652
730
|
const candidateSelectsDesiredVersion =
|
|
653
731
|
Boolean(desiredVersion) && candidateTextVersion === desiredVersion;
|
|
654
732
|
if (
|
|
@@ -695,6 +773,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
695
773
|
node?.getAttribute?.('aria-haspopup') === 'menu' ||
|
|
696
774
|
node?.getAttribute?.('data-has-submenu') !== null);
|
|
697
775
|
if (wantsPro && candidateHasThinking) return 0;
|
|
776
|
+
if (desiredModelVariant === 'sol' && candidateHasPro) return 0;
|
|
698
777
|
if (wantsPro && candidateHasLegacyProVersion && !candidateSelectsDesiredVersion) return 0;
|
|
699
778
|
if (wantsPro && !candidateHasPro && !candidateSelectsDesiredVersion) return 0;
|
|
700
779
|
if (
|
|
@@ -850,10 +929,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
850
929
|
if (getConfigurationDialog() && !configuredSelectionMatchesTarget()) return false;
|
|
851
930
|
const optionVersion = versionFromLabel(normalizedText) ?? versionFromTestId(testid);
|
|
852
931
|
if (desiredVersion && optionVersion !== desiredVersion) return false;
|
|
932
|
+
if (desiredModelVariant && !normalizedText.includes(desiredModelVariant)) return false;
|
|
933
|
+
if (
|
|
934
|
+
desiredModelVariant === 'sol' &&
|
|
935
|
+
(labelHasProWord(normalizedText) || normalizeText(testid ?? '').includes('pro'))
|
|
936
|
+
) return false;
|
|
937
|
+
if (desiredVersion === '5-6') return !hasProComposerPill();
|
|
853
938
|
const currentButtonLabel = normalizeText(getButtonLabel());
|
|
854
939
|
return !labelHasProWord(currentButtonLabel) && !hasProComposerPill();
|
|
855
940
|
};
|
|
856
941
|
const openedSubmenuKeys = new Set();
|
|
942
|
+
let clickedTargetOption = false;
|
|
857
943
|
const submenuKey = (normalizedText, testid) =>
|
|
858
944
|
normalizeText(testid ?? '') + '|' + normalizedText;
|
|
859
945
|
|
|
@@ -927,10 +1013,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
927
1013
|
});
|
|
928
1014
|
const dispatchHoverSequence = (target) => {
|
|
929
1015
|
if (!target || !(target instanceof EventTarget)) return false;
|
|
1016
|
+
const rect = target.getBoundingClientRect?.();
|
|
1017
|
+
const clientX = rect ? rect.x + rect.width / 2 : 0;
|
|
1018
|
+
const clientY = rect ? rect.y + rect.height / 2 : 0;
|
|
930
1019
|
const types = ['pointerover', 'pointerenter', 'mouseover', 'mouseenter', 'pointermove', 'mousemove'];
|
|
931
1020
|
for (const type of types) {
|
|
932
1021
|
try {
|
|
933
|
-
const common = { bubbles: true, cancelable: true, view: window };
|
|
1022
|
+
const common = { bubbles: true, cancelable: true, view: window, clientX, clientY };
|
|
934
1023
|
const event =
|
|
935
1024
|
type.startsWith('pointer') && 'PointerEvent' in window
|
|
936
1025
|
? new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' })
|
|
@@ -1002,7 +1091,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
1002
1091
|
) {
|
|
1003
1092
|
const resolvedLabel = getResolvedLabel(match.label);
|
|
1004
1093
|
closeMenu();
|
|
1005
|
-
resolve({
|
|
1094
|
+
resolve({
|
|
1095
|
+
status: clickedTargetOption ? 'switched' : 'already-selected',
|
|
1096
|
+
label: resolvedLabel,
|
|
1097
|
+
});
|
|
1006
1098
|
return;
|
|
1007
1099
|
}
|
|
1008
1100
|
const previousButtonLabel = normalizeText(getButtonLabel());
|
|
@@ -1016,6 +1108,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
1016
1108
|
setTimeout(attempt, REOPEN_INTERVAL_MS / 2);
|
|
1017
1109
|
return;
|
|
1018
1110
|
}
|
|
1111
|
+
clickedTargetOption = true;
|
|
1019
1112
|
dispatchClickSequence(match.node);
|
|
1020
1113
|
// Wait for the selected model signal to settle before reopening the picker.
|
|
1021
1114
|
waitForTargetSelection(previousButtonLabel, previousComposerSignal).then((selectionSettled) => {
|
|
@@ -1052,6 +1145,9 @@ function buildComposerSignalMatchers(targetModel) {
|
|
|
1052
1145
|
.replace(/[^a-z0-9]+/g, " ")
|
|
1053
1146
|
.replace(/\s+/g, " ")
|
|
1054
1147
|
.trim();
|
|
1148
|
+
if (normalized.includes("5 6")) {
|
|
1149
|
+
return { includesAny: ["5 6 sol"], excludesAny: ["pro"], allowBlank: false };
|
|
1150
|
+
}
|
|
1055
1151
|
if (normalized.includes("pro")) {
|
|
1056
1152
|
return { includesAny: ["pro"], excludesAny: ["thinking"], allowBlank: false };
|
|
1057
1153
|
}
|
|
@@ -1086,6 +1182,21 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
1086
1182
|
push(`chatgpt ${dotless}`, labelTokens);
|
|
1087
1183
|
push(`gpt ${base}`, labelTokens);
|
|
1088
1184
|
push(`gpt ${dotless}`, labelTokens);
|
|
1185
|
+
// Numeric variations (5.6 <-> 56 <-> gpt-5-6)
|
|
1186
|
+
if (base.includes("5.6") || base.includes("5-6") || base.includes("56")) {
|
|
1187
|
+
push("5.6", labelTokens);
|
|
1188
|
+
push("gpt-5.6", labelTokens);
|
|
1189
|
+
push("gpt5.6", labelTokens);
|
|
1190
|
+
push("gpt-5-6", labelTokens);
|
|
1191
|
+
push("gpt5-6", labelTokens);
|
|
1192
|
+
push("gpt56", labelTokens);
|
|
1193
|
+
push("gpt-5.6 sol", labelTokens);
|
|
1194
|
+
push("chatgpt 5.6", labelTokens);
|
|
1195
|
+
testIdTokens.add("model-switcher-gpt-5-6");
|
|
1196
|
+
testIdTokens.add("gpt-5-6");
|
|
1197
|
+
testIdTokens.add("gpt5-6");
|
|
1198
|
+
testIdTokens.add("gpt56");
|
|
1199
|
+
}
|
|
1089
1200
|
// Numeric variations (5.5 <-> 55 <-> gpt-5-5)
|
|
1090
1201
|
if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
|
|
1091
1202
|
push("5.5", labelTokens);
|
|
@@ -124,6 +124,172 @@ export async function navigateToPromptReadyWithFallback(Page, Runtime, options,
|
|
|
124
124
|
return { usedFallback: true };
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
|
+
function buildChatModeProbeExpression() {
|
|
128
|
+
return `(() => {
|
|
129
|
+
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
130
|
+
const isVisible = (node) => {
|
|
131
|
+
if (!(node instanceof HTMLElement)) return false;
|
|
132
|
+
const rect = node.getBoundingClientRect();
|
|
133
|
+
if (rect.width <= 0 || rect.height <= 0) return false;
|
|
134
|
+
const style = window.getComputedStyle(node);
|
|
135
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
136
|
+
};
|
|
137
|
+
const isSelected = (node) =>
|
|
138
|
+
node?.getAttribute?.('aria-checked') === 'true' ||
|
|
139
|
+
node?.getAttribute?.('data-state') === 'on';
|
|
140
|
+
const conversationIdFromPath = (value) => {
|
|
141
|
+
const match = String(value || '').match(/\\/c\\/([a-zA-Z0-9-]+)/);
|
|
142
|
+
return match?.[1] || null;
|
|
143
|
+
};
|
|
144
|
+
const isStructuredWorkBadge = (node) =>
|
|
145
|
+
node instanceof HTMLElement &&
|
|
146
|
+
node.tagName === 'SPAN' &&
|
|
147
|
+
normalize(node.textContent) === 'work' &&
|
|
148
|
+
node.childElementCount === 0 &&
|
|
149
|
+
!node.hasAttribute('dir') &&
|
|
150
|
+
node.classList.contains('shrink-0') &&
|
|
151
|
+
node.parentElement?.matches('span.flex.items-center');
|
|
152
|
+
|
|
153
|
+
const pathname = typeof location?.pathname === 'string' ? location.pathname : '';
|
|
154
|
+
const conversationId = conversationIdFromPath(pathname);
|
|
155
|
+
if (conversationId) {
|
|
156
|
+
// Conversation messages can contain same-origin links to the current thread. Only sidebar
|
|
157
|
+
// history items use ChatGPT's renderer-owned menu-item anchor class.
|
|
158
|
+
const activeHistoryLinks = Array.from(
|
|
159
|
+
document.querySelectorAll('a.__menu-item[href*="/c/"]'),
|
|
160
|
+
).filter((node) => {
|
|
161
|
+
try {
|
|
162
|
+
const candidateUrl = new URL(node.getAttribute('href') || '', location.origin);
|
|
163
|
+
return candidateUrl.origin === location.origin && conversationIdFromPath(candidateUrl.pathname) === conversationId;
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
if (activeHistoryLinks.length > 0) {
|
|
169
|
+
const hasWorkBadge = activeHistoryLinks.some((link) =>
|
|
170
|
+
Array.from(link.querySelectorAll('span')).some(isStructuredWorkBadge),
|
|
171
|
+
);
|
|
172
|
+
if (hasWorkBadge) return { status: 'work-conversation' };
|
|
173
|
+
|
|
174
|
+
const ariaLabels = activeHistoryLinks
|
|
175
|
+
.map((link) => normalize(link.getAttribute('aria-label')))
|
|
176
|
+
.filter(Boolean);
|
|
177
|
+
if (ariaLabels.length === 0 || ariaLabels.some((aria) => /,\\s*work\\s*$/.test(aria))) {
|
|
178
|
+
return { status: 'conversation-unresolved' };
|
|
179
|
+
}
|
|
180
|
+
return { status: 'chat-conversation' };
|
|
181
|
+
}
|
|
182
|
+
return { status: 'conversation-unresolved' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const radios = Array.from(document.querySelectorAll('button[role="radio"]')).filter(isVisible);
|
|
186
|
+
const chat = radios.find((node) => normalize(node.textContent) === 'chat');
|
|
187
|
+
const work = radios.find((node) => normalize(node.textContent) === 'work');
|
|
188
|
+
if (!chat && !work) return { status: 'controls-absent' };
|
|
189
|
+
if (!chat || !work) return { status: 'mode-unresolved' };
|
|
190
|
+
if (isSelected(chat)) return { status: 'chat-selected' };
|
|
191
|
+
if (isSelected(work)) {
|
|
192
|
+
const rect = chat.getBoundingClientRect();
|
|
193
|
+
return {
|
|
194
|
+
status: 'work-selected',
|
|
195
|
+
chatPoint: { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return { status: 'mode-unresolved' };
|
|
199
|
+
})()`;
|
|
200
|
+
}
|
|
201
|
+
export async function ensureChatMode(Runtime, Input, timeoutMs, logger, options = {}) {
|
|
202
|
+
const verificationWindowMs = Math.min(Math.max(0, timeoutMs), 10_000);
|
|
203
|
+
let deadline = Date.now() + verificationWindowMs;
|
|
204
|
+
const pollMs = Math.max(0, options.pollMs ?? 200);
|
|
205
|
+
let changedFromWork = false;
|
|
206
|
+
let clickedChat = false;
|
|
207
|
+
for (;;) {
|
|
208
|
+
const outcome = await Runtime.evaluate({
|
|
209
|
+
expression: buildChatModeProbeExpression(),
|
|
210
|
+
returnByValue: true,
|
|
211
|
+
});
|
|
212
|
+
const probe = outcome.result?.value;
|
|
213
|
+
if (probe?.status === "chat-selected" || probe?.status === "chat-conversation") {
|
|
214
|
+
logger(changedFromWork ? "ChatGPT mode: Chat (switched from Work)" : "ChatGPT mode: Chat");
|
|
215
|
+
return changedFromWork ? "switched" : "chat";
|
|
216
|
+
}
|
|
217
|
+
if (probe?.status === "conversation-unresolved") {
|
|
218
|
+
if (Date.now() < deadline) {
|
|
219
|
+
await delay(pollMs);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (changedFromWork)
|
|
223
|
+
break;
|
|
224
|
+
if (options.resetWorkConversation) {
|
|
225
|
+
logger("ChatGPT conversation mode unresolved; opening a new Chat");
|
|
226
|
+
await options.resetWorkConversation();
|
|
227
|
+
changedFromWork = true;
|
|
228
|
+
deadline = Date.now() + verificationWindowMs;
|
|
229
|
+
await delay(pollMs);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
throw new BrowserAutomationError("Oracle could not verify whether the selected ChatGPT conversation uses Chat or Work mode, so it cannot safely resume that conversation.", { stage: "chat-mode-selection", details: { mode: "conversation-unresolved" } });
|
|
233
|
+
}
|
|
234
|
+
if (probe?.status === "controls-absent") {
|
|
235
|
+
if (changedFromWork) {
|
|
236
|
+
if (Date.now() < deadline) {
|
|
237
|
+
await delay(pollMs);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
return "unavailable";
|
|
243
|
+
}
|
|
244
|
+
if (probe?.status === "work-conversation") {
|
|
245
|
+
if (changedFromWork)
|
|
246
|
+
break;
|
|
247
|
+
if (options.resetWorkConversation) {
|
|
248
|
+
logger("ChatGPT mode: Work conversation; opening a new Chat");
|
|
249
|
+
await options.resetWorkConversation();
|
|
250
|
+
changedFromWork = true;
|
|
251
|
+
deadline = Date.now() + verificationWindowMs;
|
|
252
|
+
await delay(pollMs);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
throw new BrowserAutomationError("The selected ChatGPT tab is an existing Work conversation and cannot be resumed as an ordinary Chat conversation. Start a new Chat conversation instead.", { stage: "chat-mode-selection", details: { mode: "work-conversation" } });
|
|
256
|
+
}
|
|
257
|
+
if (probe?.status === "work-selected" && !clickedChat && probe.chatPoint) {
|
|
258
|
+
logger("ChatGPT mode: Work; switching to Chat");
|
|
259
|
+
const { x, y } = probe.chatPoint;
|
|
260
|
+
await Input.dispatchMouseEvent({ type: "mouseMoved", x, y });
|
|
261
|
+
await Input.dispatchMouseEvent({
|
|
262
|
+
type: "mousePressed",
|
|
263
|
+
x,
|
|
264
|
+
y,
|
|
265
|
+
button: "left",
|
|
266
|
+
clickCount: 1,
|
|
267
|
+
});
|
|
268
|
+
await Input.dispatchMouseEvent({
|
|
269
|
+
type: "mouseReleased",
|
|
270
|
+
x,
|
|
271
|
+
y,
|
|
272
|
+
button: "left",
|
|
273
|
+
clickCount: 1,
|
|
274
|
+
});
|
|
275
|
+
changedFromWork = true;
|
|
276
|
+
clickedChat = true;
|
|
277
|
+
deadline = Date.now() + verificationWindowMs;
|
|
278
|
+
await delay(pollMs);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (Date.now() >= deadline)
|
|
282
|
+
break;
|
|
283
|
+
await delay(pollMs);
|
|
284
|
+
}
|
|
285
|
+
await logDomFailure(Runtime, logger, "chat-mode-selection");
|
|
286
|
+
throw new BrowserAutomationError(changedFromWork
|
|
287
|
+
? "Oracle could not verify Chat mode after leaving Work."
|
|
288
|
+
: "ChatGPT exposed Chat/Work controls but Oracle could not verify the active mode.", { stage: "chat-mode-selection", details: { changedFromWork, clickedChat } });
|
|
289
|
+
}
|
|
290
|
+
export function buildChatModeProbeExpressionForTest() {
|
|
291
|
+
return buildChatModeProbeExpression();
|
|
292
|
+
}
|
|
127
293
|
export async function ensureNotBlocked(Runtime, headless, logger) {
|
|
128
294
|
if (await isCloudflareInterstitial(Runtime)) {
|
|
129
295
|
const message = headless
|
|
@@ -456,22 +622,77 @@ async function waitForPrompt(Runtime, timeoutMs) {
|
|
|
456
622
|
}
|
|
457
623
|
return false;
|
|
458
624
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
625
|
+
// A single injected verdict. The old check flagged a challenge whenever the
|
|
626
|
+
// '/challenge-platform/' script was present, but Cloudflare bot-management injects that script on
|
|
627
|
+
// NORMAL ChatGPT pages too (including the new GPT-5.6 "Work" UI), so it false-flagged healthy pages
|
|
628
|
+
// as challenges (which then aborted the run). Classification by evidence strength:
|
|
629
|
+
// strong - the "Just a moment"/"Attention Required" title, a challenge widget, or
|
|
630
|
+
// verification copy on a short page: a real interstitial, classify immediately.
|
|
631
|
+
// shell - the ChatGPT app shell rendered: never a challenge, regardless of injected scripts.
|
|
632
|
+
// weak - only the bot-management script on a short, shell-less page. This is INFERENCE, and
|
|
633
|
+
// right after navigation it also matches a healthy SPA load mid-hydration (readyState
|
|
634
|
+
// fires before React renders the shell), so weak evidence must PERSIST through a
|
|
635
|
+
// hydration grace window before it counts (see isCloudflareInterstitial).
|
|
636
|
+
export function buildCloudflareVerdictExpression() {
|
|
637
|
+
return `(() => {
|
|
638
|
+
const title = String(document.title || '').toLowerCase();
|
|
639
|
+
const titleSaysChallenge =
|
|
640
|
+
title.includes(${JSON.stringify(CLOUDFLARE_TITLE.toLowerCase())}) ||
|
|
641
|
+
(title.includes('attention required') && title.includes('cloudflare'));
|
|
642
|
+
const hasAppShell = Boolean(document.querySelector(
|
|
643
|
+
'#prompt-textarea, [data-testid="prompt-textarea"], [data-testid^="conversation-turn"], [data-testid="profile-button"], main form[data-type], nav a[href*="/c/"]'
|
|
644
|
+
));
|
|
645
|
+
const bodyText = String((document.body && document.body.innerText) || '')
|
|
646
|
+
.toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
647
|
+
const isShortPage = bodyText.length < 600;
|
|
648
|
+
const hasChallengeWidget = Boolean(document.querySelector(
|
|
649
|
+
'#challenge-form, #challenge-running, #cf-challenge-running, [class*="cf-challenge"], iframe[src*="challenges.cloudflare.com"], iframe[src*="/cdn-cgi/challenge-platform/"]'
|
|
650
|
+
));
|
|
651
|
+
const saysVerifying = /verify(ing)? you are human|checking your browser|needs to review the security of your connection|just a moment/.test(bodyText);
|
|
652
|
+
const hasChallengeScript = Boolean(document.querySelector(${JSON.stringify(CLOUDFLARE_SCRIPT_SELECTOR)}));
|
|
653
|
+
return {
|
|
654
|
+
strong:
|
|
655
|
+
!hasAppShell &&
|
|
656
|
+
(titleSaysChallenge || hasChallengeWidget || (isShortPage && saysVerifying)),
|
|
657
|
+
shell: hasAppShell,
|
|
658
|
+
// A genuine interstitial is a SHORT page; the content-rich app never is. So the script
|
|
659
|
+
// only counts on a short page with no app shell.
|
|
660
|
+
weak: !hasAppShell && hasChallengeScript && isShortPage,
|
|
661
|
+
};
|
|
662
|
+
})()`;
|
|
663
|
+
}
|
|
664
|
+
// Boolean composition kept for tests/back-compat: what an instantaneous classifier would say.
|
|
665
|
+
export function buildCloudflareInterstitialExpression() {
|
|
666
|
+
return `(() => { const v = ${buildCloudflareVerdictExpression()}; return v.strong || v.weak; })()`;
|
|
667
|
+
}
|
|
668
|
+
export const buildCloudflareInterstitialExpressionForTest = buildCloudflareInterstitialExpression;
|
|
669
|
+
export const buildCloudflareVerdictExpressionForTest = buildCloudflareVerdictExpression;
|
|
670
|
+
// Weak (script-only) evidence must persist through a hydration grace window: callers run this
|
|
671
|
+
// right after navigation, when a healthy SPA load can briefly look exactly like the weak case
|
|
672
|
+
// (script present, shell not yet rendered, short body). A real interstitial never grows an app
|
|
673
|
+
// shell, so waiting converts the race into a correct answer at the cost of a few seconds only
|
|
674
|
+
// on genuinely ambiguous pages. Strong evidence still classifies immediately.
|
|
675
|
+
const CLOUDFLARE_HYDRATION_GRACE_MS = 12_000;
|
|
676
|
+
async function isCloudflareInterstitial(Runtime, graceMs = CLOUDFLARE_HYDRATION_GRACE_MS) {
|
|
677
|
+
const deadline = Date.now() + Math.max(0, graceMs);
|
|
678
|
+
for (;;) {
|
|
679
|
+
const { result } = await Runtime.evaluate({
|
|
680
|
+
expression: buildCloudflareVerdictExpression(),
|
|
681
|
+
returnByValue: true,
|
|
682
|
+
});
|
|
683
|
+
const verdict = (result?.value ?? {});
|
|
684
|
+
if (verdict.strong)
|
|
685
|
+
return true;
|
|
686
|
+
if (verdict.shell)
|
|
687
|
+
return false;
|
|
688
|
+
if (!verdict.weak)
|
|
689
|
+
return false;
|
|
690
|
+
if (Date.now() >= deadline)
|
|
691
|
+
return true;
|
|
692
|
+
await delay(500);
|
|
468
693
|
}
|
|
469
|
-
const { result } = await Runtime.evaluate({
|
|
470
|
-
expression: `Boolean(document.querySelector('${CLOUDFLARE_SCRIPT_SELECTOR}'))`,
|
|
471
|
-
returnByValue: true,
|
|
472
|
-
});
|
|
473
|
-
return Boolean(result.value);
|
|
474
694
|
}
|
|
695
|
+
export const isCloudflareInterstitialForTest = isCloudflareInterstitial;
|
|
475
696
|
async function isChatGptAccountSecurityBlock(Runtime) {
|
|
476
697
|
try {
|
|
477
698
|
const outcome = await Runtime.evaluate({
|