@steipete/oracle 0.19.0 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +72 -29
- package/dist/src/browser/actions/deepResearch.js +168 -44
- package/dist/src/browser/actions/modelSelection.js +78 -1
- package/dist/src/browser/actions/promptComposer.js +3 -0
- package/dist/src/browser/actions/thinkingTime.js +49 -6
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/chromeLifecycle.js +28 -16
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +144 -49
- package/dist/src/browser/liveTabs.js +11 -4
- package/dist/src/browser/profileState.js +6 -1
- package/dist/src/browser/promptFingerprint.js +54 -0
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +178 -109
- package/dist/src/browser/recoveryTarget.js +156 -0
- package/dist/src/browser/sessionRunner.js +33 -8
- package/dist/src/browser/tabLeaseRegistry.js +20 -0
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/cli/browserConfig.js +33 -3
- package/dist/src/cli/browserDefaults.js +3 -0
- package/dist/src/cli/browserTabs.js +38 -3
- package/dist/src/cli/detach.js +10 -1
- package/dist/src/cli/detachedSession.js +36 -0
- package/dist/src/cli/options.js +15 -0
- package/dist/src/cli/recoveredBrowserHarvest.js +50 -0
- package/dist/src/cli/runOptions.js +19 -4
- package/dist/src/cli/sessionDisplay.js +15 -8
- package/dist/src/cli/sessionRunner.js +153 -41
- package/dist/src/mcp/tools/consult.js +2 -2
- package/dist/src/mcp/types.js +1 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +3 -0
- package/dist/src/remote/server.js +2 -0
- package/dist/src/sessionManager.js +8 -1
- 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 +10 -10
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -81,6 +81,15 @@ function assertResolvedModelSelection(desiredModel, resolvedLabel) {
|
|
|
81
81
|
const resolved = resolvedLabel.toLowerCase();
|
|
82
82
|
const normalizedDesired = normalizeResolvedModelLabel(desired);
|
|
83
83
|
const normalizedResolved = normalizeResolvedModelLabel(resolved);
|
|
84
|
+
if (desired === "latest") {
|
|
85
|
+
// The advanced radio is localized, but only the documented exact labels are
|
|
86
|
+
// evidence of GPT-6 Astra. Do not let a generic picker result verify Latest.
|
|
87
|
+
if (resolvedLabel.normalize("NFC").trim() === "Latest" ||
|
|
88
|
+
resolvedLabel.normalize("NFC").trim() === "最新") {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-6 Astra (Latest).`);
|
|
92
|
+
}
|
|
84
93
|
const wantsGpt56Sol = /(?:^| )5 6(?: |$)/.test(normalizedDesired) && normalizedDesired.split(" ").includes("sol");
|
|
85
94
|
if (wantsGpt56Sol) {
|
|
86
95
|
const resolvedTokens = normalizedResolved.split(" ");
|
|
@@ -169,6 +178,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
169
178
|
const hasToken = (value, token) => normalizeText(value).split(' ').includes(token);
|
|
170
179
|
// Normalize every candidate token to keep fuzzy matching deterministic.
|
|
171
180
|
const normalizedTarget = normalizeText(PRIMARY_LABEL);
|
|
181
|
+
// "Latest" (GPT-6 since 2026-09) is a radio in the advanced view whose composer pill reads
|
|
182
|
+
// "6 Pro" / "6 High"…, while GPT-5.6 Sol's reads "5.6 Pro". Declared up front: getResolvedLabel
|
|
183
|
+
// runs on the picker-less path before the selection helpers below are initialized.
|
|
184
|
+
const targetIsLatest = normalizedTarget === 'latest';
|
|
185
|
+
// ChatGPT localizes the Latest radio itself (for example, Japanese "最新") but
|
|
186
|
+
// keeps the GPT-6 composer pill numeric. Keep this allow-list exact so GPT-5.6
|
|
187
|
+
// Sol or an arbitrary localized menu row can never satisfy a Latest request.
|
|
188
|
+
const isLatestModelLabel = (value) => {
|
|
189
|
+
const label = String(value ?? '').normalize('NFC').trim();
|
|
190
|
+
return label === 'Latest' || label === '最新';
|
|
191
|
+
};
|
|
172
192
|
const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
|
|
173
193
|
.map((token) => normalizeText(token))
|
|
174
194
|
.filter(Boolean);
|
|
@@ -310,6 +330,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
310
330
|
};
|
|
311
331
|
|
|
312
332
|
const getButtonLabel = () => (findModelButton()?.textContent ?? '').trim();
|
|
333
|
+
// With the picker closed the only evidence for "Latest" is the composer pill, so a version-less
|
|
334
|
+
// "latest" target must be decided on it: the blank composer signal would otherwise pass as
|
|
335
|
+
// "already selected" while GPT-5.6 Sol is active. Defined here, before getResolvedLabel, because
|
|
336
|
+
// the "current" strategy resolves the label before the selection helpers further down exist.
|
|
337
|
+
const latestButtonSelected = () => {
|
|
338
|
+
const label = normalizeText(getButtonLabel());
|
|
339
|
+
return /^(chatgpt |gpt )?6(?![0-9 .]*[0-9])/.test(label) && !/(^| )5 6/.test(label);
|
|
340
|
+
};
|
|
313
341
|
const getComposerModelLabel = () =>
|
|
314
342
|
(document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
|
|
315
343
|
const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
|
|
@@ -481,15 +509,37 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
481
509
|
if (wantsInstant) return label.includes('instant');
|
|
482
510
|
if (wantsThinking) return Boolean(desiredVersion) && !labelHasProWord(label);
|
|
483
511
|
if (desiredVersion) return true;
|
|
512
|
+
// A version-less target ("Latest") must match the radio that is actually checked in the
|
|
513
|
+
// advanced view: the opener's text lists every radio label, so a substring test would
|
|
514
|
+
// report "Latest" as selected while GPT-5.6 Sol is the checked model.
|
|
515
|
+
const checkedAdvancedRadio = findCheckedAdvancedModelRadio(parentMenu);
|
|
516
|
+
if (checkedAdvancedRadio) {
|
|
517
|
+
const checkedLabel = checkedAdvancedRadio.textContent ?? '';
|
|
518
|
+
return targetIsLatest
|
|
519
|
+
? isLatestModelLabel(checkedLabel)
|
|
520
|
+
: normalizedTokens.some((token) => token && normalizeText(checkedLabel) === token);
|
|
521
|
+
}
|
|
484
522
|
return normalizedTokens.some((token) => token && label.includes(token));
|
|
485
523
|
};
|
|
524
|
+
const findCheckedAdvancedModelRadio = (menu = null) => {
|
|
525
|
+
const scope = menu || findUnifiedPickerMenu() || document;
|
|
526
|
+
return (
|
|
527
|
+
scope?.querySelector?.(
|
|
528
|
+
'[data-testid="composer-model-picker-slider-advanced-view"] [role="menuitemradio"][aria-checked="true"]',
|
|
529
|
+
) ?? null
|
|
530
|
+
);
|
|
531
|
+
};
|
|
486
532
|
const getAdvancedModelLabel = () => {
|
|
487
533
|
const opener = findModelSubmenuOpener(findUnifiedPickerMenu());
|
|
488
534
|
if (!opener) return '';
|
|
489
535
|
const raw = (opener.textContent ?? '').trim();
|
|
490
536
|
const normalized = normalizeText(pickerNodeLabel(opener));
|
|
491
537
|
const version = versionFromLabel(normalized);
|
|
492
|
-
if (!version)
|
|
538
|
+
if (!version) {
|
|
539
|
+
const checkedAdvancedRadio = findCheckedAdvancedModelRadio(findUnifiedPickerMenu());
|
|
540
|
+
const checkedLabel = (checkedAdvancedRadio?.textContent ?? '').trim();
|
|
541
|
+
return checkedLabel || raw;
|
|
542
|
+
}
|
|
493
543
|
const [major, minor] = version.split('-');
|
|
494
544
|
const suffix = normalized.split(' ').includes('sol') ? ' Sol' : '';
|
|
495
545
|
return 'GPT-' + major + '.' + minor + suffix;
|
|
@@ -503,6 +553,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
503
553
|
);
|
|
504
554
|
};
|
|
505
555
|
const getResolvedLabel = (observedOptionLabel = '') => {
|
|
556
|
+
if (targetIsLatest) {
|
|
557
|
+
const checkedAdvancedRadio = findCheckedAdvancedModelRadio();
|
|
558
|
+
if (checkedAdvancedRadio) return (checkedAdvancedRadio.textContent ?? '').trim();
|
|
559
|
+
// Picker closed: the pill ("6 Pro") is the evidence; report the radio's name so callers
|
|
560
|
+
// can compare against the requested target instead of the tier-suffixed pill text.
|
|
561
|
+
if (latestButtonSelected()) return 'Latest';
|
|
562
|
+
const currentButtonLabel = getButtonLabel();
|
|
563
|
+
if (currentButtonLabel) return currentButtonLabel;
|
|
564
|
+
// No picker button at all (e.g. the "current" strategy on a page that hides it): fall back
|
|
565
|
+
// to the generic composer/observed label resolution below.
|
|
566
|
+
}
|
|
506
567
|
if (configuredSelectionMatchesTarget()) {
|
|
507
568
|
const variant = getConfiguredVariantLabel();
|
|
508
569
|
const version = formatModelOptionLabel(getConfiguredVersionLabel());
|
|
@@ -644,6 +705,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
644
705
|
return COMPOSER_SIGNAL_INCLUDES.some((token) => token && signal.includes(token));
|
|
645
706
|
};
|
|
646
707
|
const activeSelectionMatchesTarget = () => {
|
|
708
|
+
if (targetIsLatest) {
|
|
709
|
+
const checkedAdvancedRadio = findCheckedAdvancedModelRadio();
|
|
710
|
+
if (checkedAdvancedRadio) {
|
|
711
|
+
return isLatestModelLabel(checkedAdvancedRadio.textContent ?? '');
|
|
712
|
+
}
|
|
713
|
+
return latestButtonSelected();
|
|
714
|
+
}
|
|
647
715
|
if (advancedModelSignalMatchesTarget()) {
|
|
648
716
|
return true;
|
|
649
717
|
}
|
|
@@ -718,6 +786,11 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
718
786
|
|
|
719
787
|
const scoreOption = (normalizedText, testid, node) => {
|
|
720
788
|
// Assign a score to every node so we can pick the most likely match without brittle equality checks.
|
|
789
|
+
// Latest is localized in the advanced radio list. Match the documented labels
|
|
790
|
+
// exactly instead of falling through to generic scoring, which could select Sol.
|
|
791
|
+
if (targetIsLatest) {
|
|
792
|
+
return isLatestModelLabel(node?.textContent ?? '') ? 2000 : 0;
|
|
793
|
+
}
|
|
721
794
|
if (!normalizedText && !testid) {
|
|
722
795
|
return 0;
|
|
723
796
|
}
|
|
@@ -1368,6 +1441,10 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
1368
1441
|
testIdTokens.add("gpt5-6");
|
|
1369
1442
|
testIdTokens.add("gpt56");
|
|
1370
1443
|
}
|
|
1444
|
+
if (base === "latest") {
|
|
1445
|
+
// ChatGPT's Japanese advanced-model radio is named exactly "最新".
|
|
1446
|
+
push("最新", labelTokens);
|
|
1447
|
+
}
|
|
1371
1448
|
// Numeric variations (5.5 <-> 55 <-> gpt-5-5)
|
|
1372
1449
|
if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
|
|
1373
1450
|
push("5.5", labelTokens);
|
|
@@ -11,6 +11,7 @@ import { stageAttachmentPrompt } from "./attachmentPrompt.js";
|
|
|
11
11
|
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
12
12
|
import { buildAttachmentEvidenceExpression } from "./attachmentEvidence.js";
|
|
13
13
|
import { buildAttachmentProgressExpression } from "./attachmentProgress.js";
|
|
14
|
+
import { activateWebSearch } from "./webSearch.js";
|
|
14
15
|
const ENTER_KEY_EVENT = {
|
|
15
16
|
key: "Enter",
|
|
16
17
|
code: "Enter",
|
|
@@ -196,6 +197,8 @@ export async function submitPrompt(deps, prompt, logger) {
|
|
|
196
197
|
observedLength,
|
|
197
198
|
});
|
|
198
199
|
}
|
|
200
|
+
if (deps.webSearch)
|
|
201
|
+
await activateWebSearch(runtime, input, prompt, logger);
|
|
199
202
|
const clicked = await attemptSendButton(runtime, input, logger, deps?.attachmentNames, deps?.attachmentTimeoutMs, deps?.page, deps?.attachmentNavigationUrl);
|
|
200
203
|
if (!clicked) {
|
|
201
204
|
await dispatchEnterKey(input);
|
|
@@ -192,6 +192,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
192
192
|
const targetLevelLiteral = JSON.stringify(level.toLowerCase());
|
|
193
193
|
const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
|
|
194
194
|
const targetIsGpt56ModelLiteral = JSON.stringify(/(?:^|[^0-9])5[._ -]6(?:[^0-9]|$)/i.test(desiredModel ?? ""));
|
|
195
|
+
// Astra resolves the gpt-6-pro request to this exact model-picker alias. Keep
|
|
196
|
+
// this deliberately narrow: a generic unknown model must not claim its
|
|
197
|
+
// version-prefixed effort pill whose ownership we cannot establish.
|
|
198
|
+
const targetIsAstraLatestLiteral = JSON.stringify((desiredModel ?? "").trim().toLowerCase() === "latest");
|
|
195
199
|
return `(async () => {
|
|
196
200
|
${buildClickDispatcher()}
|
|
197
201
|
|
|
@@ -201,13 +205,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
201
205
|
const TARGET_LEVEL = ${targetLevelLiteral};
|
|
202
206
|
const TARGET_MODEL_KIND = ${targetModelKindLiteral};
|
|
203
207
|
const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
|
|
208
|
+
const TARGET_IS_ASTRA_LATEST = ${targetIsAstraLatestLiteral};
|
|
204
209
|
|
|
205
210
|
// Multilingual matchers: English level token + observed localized variants.
|
|
206
211
|
const LEVEL_TOKENS = {
|
|
207
212
|
light: ['light', 'instant', 'sofort', 'leicht', '最速', '轻', '极速', '즉시'],
|
|
208
213
|
standard: ['standard', 'medium', 'mittel', '中程度', '标准', '中', '중간'],
|
|
209
214
|
extended: ['extended', 'high', 'hoch', 'erweitert', '高い', '扩展', '深度', '加强', '高', '높음'],
|
|
210
|
-
'extra-high': ['extra high', 'sehr hoch', '非常に高い', '极高', '매우 높음'],
|
|
215
|
+
'extra-high': ['extra high', 'sehr hoch', '非常に高い', '極高', '极高', '매우 높음'],
|
|
211
216
|
heavy: ['heavy', 'schwer', '重度', '加重'],
|
|
212
217
|
};
|
|
213
218
|
// Pro is a tier you can request, but it is also a MODEL name, so it must never
|
|
@@ -216,6 +221,29 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
216
221
|
// "Instant"/"Pro" would look like a tier list. Keep it to target matching only.
|
|
217
222
|
const TARGET_LEVEL_TOKENS = { ...LEVEL_TOKENS, pro: ['pro'] };
|
|
218
223
|
const targetTokens = TARGET_LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
|
|
224
|
+
const normalizeAstraLabel = (value) =>
|
|
225
|
+
String(value ?? '')
|
|
226
|
+
.normalize('NFC')
|
|
227
|
+
.toLowerCase()
|
|
228
|
+
.split(String.fromCharCode(9)).join(' ')
|
|
229
|
+
.split(String.fromCharCode(10)).join(' ')
|
|
230
|
+
.split(String.fromCharCode(13)).join(' ')
|
|
231
|
+
.split(String.fromCharCode(12)).join(' ')
|
|
232
|
+
.split(' ').filter(Boolean).join(' ');
|
|
233
|
+
const isAstraLatestEffortPill = (label) =>
|
|
234
|
+
TARGET_IS_ASTRA_LATEST &&
|
|
235
|
+
Object.values(TARGET_LEVEL_TOKENS).some((tokens) =>
|
|
236
|
+
tokens.some((token) => {
|
|
237
|
+
// Do not use the generic matcher here: ownership needs an exact
|
|
238
|
+
// version prefix plus a known localized tier, not token containment.
|
|
239
|
+
const tier = normalizeAstraLabel(token);
|
|
240
|
+
const observed = normalizeAstraLabel(label);
|
|
241
|
+
return observed === '6 ' + tier || observed === '6' + tier;
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
const isSolModelPillForLatest = (label) =>
|
|
245
|
+
TARGET_IS_ASTRA_LATEST &&
|
|
246
|
+
['5.6 pro', '5.6pro', '5 6 pro'].includes(normalizeAstraLabel(label));
|
|
219
247
|
|
|
220
248
|
const INITIAL_WAIT_MS = 150;
|
|
221
249
|
const STEP_WAIT_MS = 200;
|
|
@@ -1002,11 +1030,12 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1002
1030
|
.filter(Boolean);
|
|
1003
1031
|
if (selections.length !== 1) return null;
|
|
1004
1032
|
const { label, index, level } = selections[0];
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1033
|
+
// Quota-limited accounts expose four tiers; the fourth remains Extra High.
|
|
1034
|
+
// Require an observed range and matching label/index before trusting either.
|
|
1035
|
+
const maximum = thumb.getAttribute('aria-valuemax');
|
|
1036
|
+
if (thumb.getAttribute('aria-valuemin') !== '0' || !['3', '4'].includes(maximum) ||
|
|
1037
|
+
index > Number(maximum) || thumb.getAttribute('aria-valuenow') !== String(index)) return null;
|
|
1038
|
+
return { control, label, index, level, maximum: Number(maximum) };
|
|
1010
1039
|
};
|
|
1011
1040
|
let current = resolve();
|
|
1012
1041
|
const finish = (result) => { closeOpenMenus(); return result; };
|
|
@@ -1026,6 +1055,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1026
1055
|
}
|
|
1027
1056
|
return finish(failure('option-not-found'));
|
|
1028
1057
|
}
|
|
1058
|
+
const unavailable = () => finish(failure('option-disabled', {
|
|
1059
|
+
label: 'Pro', notice: 'the available four-tier effort slider does not include Pro',
|
|
1060
|
+
}));
|
|
1061
|
+
if (targetIndex > current.maximum) return unavailable();
|
|
1029
1062
|
if (current.level === target) return finish({ status: 'already-selected', label: current.label });
|
|
1030
1063
|
const deadline = performance.now() + MAX_WAIT_MS;
|
|
1031
1064
|
for (let attempt = 0; attempt < 4 && performance.now() < deadline; attempt += 1) {
|
|
@@ -1041,6 +1074,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1041
1074
|
if (next && next.index !== previousIndex) { current = next; break; }
|
|
1042
1075
|
}
|
|
1043
1076
|
if (!current) return finish(failure('selection-unverified'));
|
|
1077
|
+
if (targetIndex > current.maximum) return unavailable();
|
|
1044
1078
|
if (current.level === target) return finish({ status: 'switched', label: current.label });
|
|
1045
1079
|
}
|
|
1046
1080
|
return finish(failure('selection-unverified'));
|
|
@@ -1062,6 +1096,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1062
1096
|
if (seen.has(button) || !isVisible(button)) continue;
|
|
1063
1097
|
seen.add(button);
|
|
1064
1098
|
if (button.getAttribute?.('data-testid') === 'model-switcher-dropdown-button') continue;
|
|
1099
|
+
// A 5.6 Pro model pill is not Astra Latest's 6-prefixed effort owner.
|
|
1100
|
+
// Keep this rejection ahead of the generic compatibility matcher.
|
|
1101
|
+
if (isSolModelPillForLatest(button.textContent ?? '')) continue;
|
|
1065
1102
|
const label = normalize(
|
|
1066
1103
|
(button.getAttribute?.('aria-label') ?? '') + ' ' +
|
|
1067
1104
|
(button.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
@@ -1071,6 +1108,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1071
1108
|
(TARGET_MODEL_KIND === 'pro' && hasToken(label, 'pro') && !hasToken(label, 'thinking')) ||
|
|
1072
1109
|
(TARGET_MODEL_KIND === 'thinking' && hasToken(label, 'thinking') && !hasToken(label, 'pro')) ||
|
|
1073
1110
|
(!TARGET_MODEL_KIND && hasToken(label, 'thinking')) ||
|
|
1111
|
+
// Astra Latest prefixes a supported effort label with "6" (for
|
|
1112
|
+
// example, "6 Pro" or textContent-concatenated "6Pro"). This is
|
|
1113
|
+
// recognized only for the exact Latest target; selection still
|
|
1114
|
+
// requires the direct slider's leading label and numeric ARIA proof.
|
|
1115
|
+
isAstraLatestEffortPill(button.textContent ?? '') ||
|
|
1074
1116
|
(button.matches?.('button.__composer-pill') && matchesAnyEffortLevel(label))
|
|
1075
1117
|
) {
|
|
1076
1118
|
return button;
|
|
@@ -1157,6 +1199,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1157
1199
|
tokens.some((token) => normalize(token) === pillLabel),
|
|
1158
1200
|
);
|
|
1159
1201
|
const pillNamesEffortNotModel =
|
|
1202
|
+
TARGET_IS_ASTRA_LATEST ||
|
|
1160
1203
|
TARGET_IS_GPT56_MODEL ||
|
|
1161
1204
|
(pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
|
|
1162
1205
|
const composerModelKind =
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
2
|
+
import { INPUT_SELECTORS } from "../constants.js";
|
|
3
|
+
import { delay } from "../utils.js";
|
|
4
|
+
import { activateComposerPlus, captureComposerNavigationUrl, assertComposerPlusStayedInPlace, } from "./attachments.js";
|
|
5
|
+
import { buildComposerNavigationValidationExpression } from "./attachmentContext.js";
|
|
6
|
+
import { buildClickDispatcher } from "./domEvents.js";
|
|
7
|
+
export function matchesWebSearchMenuLabel(value) {
|
|
8
|
+
return [
|
|
9
|
+
"search",
|
|
10
|
+
"searchfindontheweb",
|
|
11
|
+
"websearch",
|
|
12
|
+
"websearchfindreal-timenewsandinfo",
|
|
13
|
+
].includes(value.replace(/\s+/g, "").toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
export function buildWebSearchVerificationExpression(prompt) {
|
|
16
|
+
return `(() => {
|
|
17
|
+
const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
|
|
18
|
+
const editor = ${JSON.stringify(INPUT_SELECTORS)}.flatMap(selector => Array.from(document.querySelectorAll(selector))).find(visible);
|
|
19
|
+
if (!editor) return { selected: false, promptMatches: false };
|
|
20
|
+
const chip = editor.querySelector('[data-inline-selection-pill][data-id="search"][data-system-hint-type="search"]');
|
|
21
|
+
const copy = editor.cloneNode(true);
|
|
22
|
+
copy.querySelectorAll('[data-inline-selection-pill], [data-inline-selection-pill-cursor-target]').forEach(node => node.remove());
|
|
23
|
+
const readText = node => {
|
|
24
|
+
if (node.nodeType === 3) return node.textContent ?? '';
|
|
25
|
+
const text = Array.from(node.childNodes).map(readText).join('');
|
|
26
|
+
return text + (['P', 'DIV', 'BR', 'LI', 'PRE'].includes(node.nodeName) ? '\\n' : '');
|
|
27
|
+
};
|
|
28
|
+
const normalize = text => String(text ?? '').replace(/[\\u200b\\ufeff]/g, '').replace(/\\s+/g, ' ').trim();
|
|
29
|
+
return { selected: Boolean(chip && visible(chip)), promptMatches: normalize(readText(copy)) === normalize(${JSON.stringify(prompt)}) };
|
|
30
|
+
})()`;
|
|
31
|
+
}
|
|
32
|
+
export function buildWebSearchSelectionExpression(navigationUrl) {
|
|
33
|
+
return `(() => {
|
|
34
|
+
${buildClickDispatcher()}
|
|
35
|
+
const matchesLabel = ${matchesWebSearchMenuLabel.toString()};
|
|
36
|
+
const navigation = ${buildComposerNavigationValidationExpression(navigationUrl)};
|
|
37
|
+
if (!navigation.contextMatches || navigation.workSelected || navigation.modeUnverified) return 'context-changed';
|
|
38
|
+
const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
|
|
39
|
+
const roots = Array.from(document.querySelectorAll('main .popover, [data-radix-popper-content-wrapper], [data-floating-ui-portal], [role="menu"], [role="listbox"]')).filter(visible);
|
|
40
|
+
const candidates = roots.flatMap(root => Array.from(root.querySelectorAll('[data-radix-collection-item], [role="menuitem"], [role="option"], .__menu-item, [class*="menu-item"]')));
|
|
41
|
+
const match = candidates.find(node => {
|
|
42
|
+
if (!visible(node) || node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true') return false;
|
|
43
|
+
return matchesLabel(node.textContent ?? '');
|
|
44
|
+
});
|
|
45
|
+
if (!match) return 'missing';
|
|
46
|
+
dispatchClickSequence(match);
|
|
47
|
+
return 'clicked';
|
|
48
|
+
})()`;
|
|
49
|
+
}
|
|
50
|
+
/** Web Search is an inline editor hint, so activate it after staging text/attachments. */
|
|
51
|
+
export async function activateWebSearch(runtime, input, prompt, logger) {
|
|
52
|
+
const navigationUrl = await captureComposerNavigationUrl(runtime);
|
|
53
|
+
const verify = async () => {
|
|
54
|
+
const { result, exceptionDetails } = await runtime.evaluate({
|
|
55
|
+
expression: buildWebSearchVerificationExpression(prompt),
|
|
56
|
+
returnByValue: true,
|
|
57
|
+
});
|
|
58
|
+
if (exceptionDetails)
|
|
59
|
+
return false;
|
|
60
|
+
return result?.value?.selected === true && result?.value?.promptMatches === true;
|
|
61
|
+
};
|
|
62
|
+
if (await verify())
|
|
63
|
+
return;
|
|
64
|
+
const activated = await activateComposerPlus(runtime, input, navigationUrl);
|
|
65
|
+
if (activated.method === "unavailable")
|
|
66
|
+
throw new BrowserAutomationError("Web Search requires the ChatGPT composer tools menu.", {
|
|
67
|
+
stage: "web-search-activate",
|
|
68
|
+
});
|
|
69
|
+
const deadline = Date.now() + 5_000;
|
|
70
|
+
let clicked = false;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
const outcome = await runtime.evaluate({
|
|
73
|
+
expression: buildWebSearchSelectionExpression(navigationUrl),
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
});
|
|
76
|
+
if (outcome.result?.value === "clicked") {
|
|
77
|
+
clicked = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (outcome.exceptionDetails || outcome.result?.value !== "missing")
|
|
81
|
+
break;
|
|
82
|
+
await delay(100);
|
|
83
|
+
}
|
|
84
|
+
if (clicked) {
|
|
85
|
+
const confirmationDeadline = Date.now() + 3_000;
|
|
86
|
+
do {
|
|
87
|
+
await assertComposerPlusStayedInPlace(runtime, navigationUrl);
|
|
88
|
+
if (await verify()) {
|
|
89
|
+
logger("Web Search selected; inline search hint and staged prompt verified.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await delay(100);
|
|
93
|
+
} while (Date.now() < confirmationDeadline);
|
|
94
|
+
}
|
|
95
|
+
throw new BrowserAutomationError("Web Search selection could not be verified; the prompt was not submitted. This pilot supports the English ChatGPT Web search control.", { stage: "web-search-activate" });
|
|
96
|
+
}
|
|
@@ -7,6 +7,7 @@ import { launch, Launcher, } from "chrome-launcher";
|
|
|
7
7
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
8
8
|
import { delay } from "./utils.js";
|
|
9
9
|
import { isWsl, resolveWslChromeLaunchRoute } from "./wslHost.js";
|
|
10
|
+
import { BrowserCancellation } from "./cancellation.js";
|
|
10
11
|
export async function launchChrome(config, userDataDir, logger) {
|
|
11
12
|
const { connectHost, debugBindAddress, usePatchedLauncher } = resolveWslChromeLaunchRoute();
|
|
12
13
|
const debugPort = config.debugPort ?? parseDebugPortEnv();
|
|
@@ -352,9 +353,10 @@ export async function connectToRemoteChrome(host, port, logger, targetUrl, brows
|
|
|
352
353
|
return {
|
|
353
354
|
client: targetConnection.client,
|
|
354
355
|
targetId: targetConnection.targetId,
|
|
355
|
-
close: async () => {
|
|
356
|
+
close: async (closeOptions) => {
|
|
356
357
|
await targetConnection.client.close().catch(() => undefined);
|
|
357
|
-
|
|
358
|
+
if (!closeOptions?.preserveTarget)
|
|
359
|
+
await closeRemoteChromeTarget(host, port, targetConnection.targetId, logger);
|
|
358
360
|
},
|
|
359
361
|
};
|
|
360
362
|
}
|
|
@@ -387,21 +389,31 @@ export async function closeRemoteChromeTarget(host, port, targetId, logger) {
|
|
|
387
389
|
}
|
|
388
390
|
}
|
|
389
391
|
export async function listRemoteChromeTargets(options) {
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return targets;
|
|
393
|
-
}
|
|
394
|
-
const browser = await connectToBrowserWebSocket(options.host, options.port, options.browserWSEndpoint, options.logger ?? (() => { }), options.approvalWaitMs);
|
|
392
|
+
const logger = options.logger ?? (() => { });
|
|
393
|
+
const cancellation = new BrowserCancellation(options.signal, logger);
|
|
395
394
|
try {
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
395
|
+
return await cancellation.run(async () => {
|
|
396
|
+
if (!options.browserWSEndpoint) {
|
|
397
|
+
const targets = await cancellation.call(() => CDP.List({ host: options.host, port: options.port }));
|
|
398
|
+
return targets;
|
|
399
|
+
}
|
|
400
|
+
const browser = await cancellation.acquire(() => connectToBrowserWebSocket(options.host, options.port, options.browserWSEndpoint, logger, options.approvalWaitMs), (lateBrowser) => lateBrowser.close());
|
|
401
|
+
try {
|
|
402
|
+
const client = cancellation.client(browser);
|
|
403
|
+
const result = await client.Target.getTargets();
|
|
404
|
+
return (result.targetInfos ?? []).map((target) => ({
|
|
405
|
+
targetId: target.targetId,
|
|
406
|
+
type: target.type,
|
|
407
|
+
url: target.url,
|
|
408
|
+
}));
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
await browser.close().catch(() => undefined);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
402
414
|
}
|
|
403
415
|
finally {
|
|
404
|
-
|
|
416
|
+
cancellation.dispose();
|
|
405
417
|
}
|
|
406
418
|
}
|
|
407
419
|
export async function connectToRemoteChromeTarget(host, port, logger, options) {
|
|
@@ -431,9 +443,9 @@ export async function connectToRemoteChromeTarget(host, port, logger, options) {
|
|
|
431
443
|
client,
|
|
432
444
|
targetId,
|
|
433
445
|
browserWSEndpoint: options.browserWSEndpoint,
|
|
434
|
-
close: async () => {
|
|
446
|
+
close: async (closeOptions) => {
|
|
435
447
|
await browser.Target.detachFromTarget({ sessionId: attached.sessionId }).catch(() => undefined);
|
|
436
|
-
if (options.closeTargetOnDispose && targetId) {
|
|
448
|
+
if (options.closeTargetOnDispose && targetId && !closeOptions?.preserveTarget) {
|
|
437
449
|
await browser.Target.closeTarget({ targetId }).catch(() => undefined);
|
|
438
450
|
}
|
|
439
451
|
await browser.close().catch(() => undefined);
|
|
@@ -123,7 +123,7 @@ export function resolveBrowserConfig(config) {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
function normalizeResearchMode(value) {
|
|
126
|
-
return value === "deep"
|
|
126
|
+
return value === "deep" || value === "search" ? value : "off";
|
|
127
127
|
}
|
|
128
128
|
function normalizeArchiveMode(value) {
|
|
129
129
|
return value === "always" || value === "never" ? value : "auto";
|