@steipete/oracle 0.19.0 → 0.20.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 +11 -13
- 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 +38 -1
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/chromeLifecycle.js +5 -4
- package/dist/src/browser/config.js +1 -1
- package/dist/src/browser/index.js +111 -43
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/providers/chatgptDomProvider.js +1 -0
- package/dist/src/browser/reattach.js +21 -1
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +3 -1
- 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/browserTabs.js +3 -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 +4 -5
- package/dist/src/cli/sessionRunner.js +4 -5
- 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 +1 -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 +3 -3
- 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-cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { CHATGPT_URL } from "../src/browser/constants.js";
|
|
|
13
13
|
import { applyHelpStyling } from "../src/cli/help.js";
|
|
14
14
|
import { collectPaths, collectModelList, collectTextValues, parseFloatOption, parseIntOption, parseSearchOption, parseThinkingTimeOption, usesDefaultStatusFilters, resolvePreviewMode, normalizeModelOption, normalizeBaseUrl, resolveApiModel, inferModelFromLabel, parseHeartbeatOption, parseTimeoutOption, parseDurationOption, mergePathLikeOptions, dedupePathInputs, } from "../src/cli/options.js";
|
|
15
15
|
import { copyToClipboard } from "../src/cli/clipboard.js";
|
|
16
|
+
import { isGpt6ProAlias } from "../src/cli/browserConfig.js";
|
|
16
17
|
import { buildMarkdownBundle } from "../src/cli/markdownBundle.js";
|
|
17
18
|
import { shouldDetachSession, stopDetachedWorker } from "../src/cli/detach.js";
|
|
18
19
|
import { launchDetachedSession } from "../src/cli/detachedSession.js";
|
|
@@ -225,15 +226,8 @@ program
|
|
|
225
226
|
.addOption(new Option("--models <models>", 'Comma-separated API model list to query in parallel (e.g., "gpt-5.5-pro,gemini-3-pro").')
|
|
226
227
|
.argParser(collectModelList)
|
|
227
228
|
.default([]))
|
|
228
|
-
.addOption(new Option("--reasoning-effort <effort>", "Reasoning effort for GPT-5.6 API models.").choices([
|
|
229
|
-
"
|
|
230
|
-
"low",
|
|
231
|
-
"medium",
|
|
232
|
-
"high",
|
|
233
|
-
"xhigh",
|
|
234
|
-
"max",
|
|
235
|
-
]))
|
|
236
|
-
.addOption(new Option("--reasoning-mode <mode>", 'Responses API reasoning execution mode for GPT-5.6 models ("standard" or "pro").').choices(["standard", "pro"]))
|
|
229
|
+
.addOption(new Option("--reasoning-effort <effort>", "Reasoning effort for GPT-6 Astra and GPT-5.6 API models (Astra requires low or higher).").choices(["none", "low", "medium", "high", "xhigh", "max"]))
|
|
230
|
+
.addOption(new Option("--reasoning-mode <mode>", 'Responses API reasoning execution mode for GPT-6 Astra and GPT-5.6 models ("standard" or "pro").').choices(["standard", "pro"]))
|
|
237
231
|
.addOption(new Option("-e, --engine <mode>", "Execution engine (api | browser). Browser engine: GPT models automate ChatGPT; Gemini models use a cookie-based client for gemini.google.com. If omitted, oracle picks api when OPENAI_API_KEY is set, otherwise browser.").choices(["api", "browser"]))
|
|
238
232
|
.addOption(new Option("--mode <mode>", "Alias for --engine (api | browser).")
|
|
239
233
|
.choices(["api", "browser"])
|
|
@@ -339,7 +333,7 @@ program
|
|
|
339
333
|
.addOption(new Option("--browser-thinking-time <level>", "Thinking time intensity for Thinking/Pro models: light, standard, extended, extra-high (Extra High), pro (Pro tier of the active model), heavy, or ChatGPT UI aliases.")
|
|
340
334
|
.argParser(parseThinkingTimeOption)
|
|
341
335
|
.hideHelp())
|
|
342
|
-
.addOption(new Option("--browser-research <mode>", "Browser research mode: deep activates
|
|
336
|
+
.addOption(new Option("--browser-research <mode>", "Browser research mode: search activates Web Search; deep activates Deep Research.").choices(["off", "search", "deep"]))
|
|
343
337
|
.addOption(new Option("--browser-archive <mode>", "Archive completed ChatGPT browser conversations after local artifacts are saved (auto archives successful non-project one-shots only).").choices(["auto", "always", "never"]))
|
|
344
338
|
.addOption(new Option("--browser-follow-up <prompt>", "Submit an additional prompt in the same ChatGPT browser conversation after the initial answer; repeat for multi-turn consults.")
|
|
345
339
|
.argParser(collectTextValues)
|
|
@@ -1064,9 +1058,13 @@ async function runRootCommand(options) {
|
|
|
1064
1058
|
options.baseUrl = userConfig.apiBaseUrl;
|
|
1065
1059
|
}
|
|
1066
1060
|
const providerMode = resolveApiProviderMode(options);
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1061
|
+
// Engine discovery must not apply API-only validation to browser aliases.
|
|
1062
|
+
const engineModelInputs = multiModelProvided
|
|
1063
|
+
? options.models
|
|
1064
|
+
: [normalizeModelOption(options.model) || DEFAULT_MODEL];
|
|
1065
|
+
const engineModels = Array.from(new Set(engineModelInputs.map((entry) => isGpt6ProAlias(entry) && !options.route && !options.preflight
|
|
1066
|
+
? "gpt-6-pro"
|
|
1067
|
+
: resolveApiModel(entry))));
|
|
1070
1068
|
if (options.route || options.preflight) {
|
|
1071
1069
|
const routeAzureEndpoint = firstNonEmpty(options.azureEndpoint, process.env.AZURE_OPENAI_ENDPOINT, userConfig.azure?.endpoint);
|
|
1072
1070
|
const configuredAzureForRoute = routeAzureEndpoint
|
|
@@ -79,65 +79,131 @@ async function waitForDeepResearchPill(Runtime, timeoutMs = 5000) {
|
|
|
79
79
|
* After prompt submission, waits for the research plan to appear and
|
|
80
80
|
* auto-confirm (~60s countdown + 10s safety margin).
|
|
81
81
|
*/
|
|
82
|
-
export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS) {
|
|
82
|
+
export async function waitForResearchPlanAutoConfirm(Runtime, logger, autoConfirmWaitMs = DEEP_RESEARCH_AUTO_CONFIRM_WAIT_MS, options) {
|
|
83
|
+
const ignoredTargetKeys = new Set(options?.ignoredTargetKeys ?? []);
|
|
84
|
+
const minTurnIndex = typeof options?.minTurnIndex === "number" && Number.isFinite(options.minTurnIndex)
|
|
85
|
+
? Math.floor(options.minTurnIndex)
|
|
86
|
+
: -1;
|
|
87
|
+
let capturedPlan = null;
|
|
88
|
+
let loggedPlan = false;
|
|
89
|
+
const targetOwnerMinTurnIndex = minTurnIndex >= 0 && options?.targetBaselineCaptured !== true ? minTurnIndex : -1;
|
|
90
|
+
const readPlanStatus = async () => {
|
|
91
|
+
const targetRead = options?.client
|
|
92
|
+
? ((await readDeepResearchTargetResult(options.client, ignoredTargetKeys, targetOwnerMinTurnIndex).catch(() => null))?.read ?? null)
|
|
93
|
+
: null;
|
|
94
|
+
if (targetRead?.planTitle || targetRead?.researchStarted) {
|
|
95
|
+
return targetRead;
|
|
96
|
+
}
|
|
97
|
+
const inPageRead = options?.Page
|
|
98
|
+
? await readDeepResearchFrameResult(Runtime, options.Page, options.client, minTurnIndex).catch(() => null)
|
|
99
|
+
: null;
|
|
100
|
+
return inPageRead?.read ?? targetRead;
|
|
101
|
+
};
|
|
102
|
+
const capturePlan = async (status) => {
|
|
103
|
+
if (!status.planTitle || !status.planSteps || status.planSteps.length === 0) {
|
|
104
|
+
return capturedPlan;
|
|
105
|
+
}
|
|
106
|
+
const next = {
|
|
107
|
+
title: status.planTitle,
|
|
108
|
+
steps: status.planSteps,
|
|
109
|
+
phase: status.researchStarted ? "researching" : "planning",
|
|
110
|
+
...(status.planActionText ? { actionText: status.planActionText } : {}),
|
|
111
|
+
capturedAt: capturedPlan?.capturedAt ?? new Date().toISOString(),
|
|
112
|
+
};
|
|
113
|
+
const changed = !capturedPlan ||
|
|
114
|
+
capturedPlan.phase !== next.phase ||
|
|
115
|
+
capturedPlan.title !== next.title ||
|
|
116
|
+
capturedPlan.steps.join("\n") !== next.steps.join("\n") ||
|
|
117
|
+
capturedPlan.actionText !== next.actionText;
|
|
118
|
+
capturedPlan = next;
|
|
119
|
+
if (!loggedPlan) {
|
|
120
|
+
logger(`[browser] Deep Research plan detected:\n${[next.title, ...next.steps.map((step, index) => `${index + 1}. ${step}`)].join("\n")}`);
|
|
121
|
+
loggedPlan = true;
|
|
122
|
+
}
|
|
123
|
+
if (changed) {
|
|
124
|
+
await options?.onPlan?.(next);
|
|
125
|
+
}
|
|
126
|
+
return next;
|
|
127
|
+
};
|
|
128
|
+
const reportResearchStarted = async () => {
|
|
129
|
+
if (capturedPlan?.phase === "planning") {
|
|
130
|
+
capturedPlan = { ...capturedPlan, phase: "researching" };
|
|
131
|
+
await options?.onPlan?.(capturedPlan);
|
|
132
|
+
}
|
|
133
|
+
logger("[browser] Deep Research execution started; plan countdown is complete.");
|
|
134
|
+
return capturedPlan;
|
|
135
|
+
};
|
|
83
136
|
// Phase A: Detect research plan appearance (up to 60s)
|
|
84
137
|
const planDeadline = Date.now() + 60_000;
|
|
85
138
|
let planDetected = false;
|
|
86
139
|
while (Date.now() < planDeadline) {
|
|
140
|
+
const frameStatus = await readPlanStatus();
|
|
141
|
+
if (frameStatus) {
|
|
142
|
+
await capturePlan(frameStatus);
|
|
143
|
+
if (frameStatus.researchStarted) {
|
|
144
|
+
return reportResearchStarted();
|
|
145
|
+
}
|
|
146
|
+
if (capturedPlan) {
|
|
147
|
+
planDetected = true;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Legacy/inline fallback. Do not treat an arbitrary large iframe as a plan:
|
|
152
|
+
// ChatGPT projects, attachments, and other tools also render large iframes.
|
|
87
153
|
const { result } = await Runtime.evaluate({
|
|
88
154
|
expression: `(() => {
|
|
89
|
-
const
|
|
90
|
-
const
|
|
91
|
-
const rect = f.getBoundingClientRect();
|
|
92
|
-
return rect.width > 200 && rect.height > 200;
|
|
93
|
-
});
|
|
94
|
-
const assistantText = (document.querySelector('[data-message-author-role="assistant"]')?.textContent || '').toLowerCase();
|
|
155
|
+
const turns = Array.from(document.querySelectorAll('[data-message-author-role="assistant"]'));
|
|
156
|
+
const assistantText = String(turns.at(-1)?.textContent || '').toLowerCase();
|
|
95
157
|
const hasResearchText = assistantText.includes('researching') ||
|
|
96
158
|
assistantText.includes('research plan') ||
|
|
97
|
-
assistantText.includes('
|
|
98
|
-
assistantText.includes('
|
|
99
|
-
return {
|
|
159
|
+
assistantText.includes('正在研究') ||
|
|
160
|
+
assistantText.includes('研究计划');
|
|
161
|
+
return { hasResearchText };
|
|
100
162
|
})()`,
|
|
101
163
|
returnByValue: true,
|
|
102
164
|
});
|
|
103
165
|
const val = result?.value;
|
|
104
|
-
if (val?.
|
|
166
|
+
if (val?.hasResearchText) {
|
|
105
167
|
planDetected = true;
|
|
106
|
-
logger("Research
|
|
168
|
+
logger("[browser] Deep Research activity detected; waiting for plan auto-confirm...");
|
|
107
169
|
break;
|
|
108
170
|
}
|
|
109
171
|
await delay(2_000);
|
|
110
172
|
}
|
|
111
173
|
if (!planDetected) {
|
|
112
174
|
logger("Warning: Research plan not detected within 60s; continuing (may have auto-confirmed already)");
|
|
113
|
-
return;
|
|
175
|
+
return capturedPlan;
|
|
114
176
|
}
|
|
115
|
-
// Phase B: Wait for
|
|
177
|
+
// Phase B: Wait for the OOPIF's real execution state instead of sleeping for
|
|
178
|
+
// the full countdown. The main page cannot see this sandboxed iframe's text.
|
|
116
179
|
const confirmStart = Date.now();
|
|
117
180
|
while (Date.now() - confirmStart < autoConfirmWaitMs) {
|
|
181
|
+
const frameStatus = await readPlanStatus();
|
|
182
|
+
if (frameStatus) {
|
|
183
|
+
await capturePlan(frameStatus);
|
|
184
|
+
if (frameStatus.researchStarted) {
|
|
185
|
+
return reportResearchStarted();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
118
188
|
const { result } = await Runtime.evaluate({
|
|
119
189
|
expression: `(() => {
|
|
120
|
-
const iframes = document.querySelectorAll('iframe');
|
|
121
|
-
const hasLargeIframe = Array.from(iframes).some(f => {
|
|
122
|
-
const rect = f.getBoundingClientRect();
|
|
123
|
-
return rect.width > 200 && rect.height > 200;
|
|
124
|
-
});
|
|
125
190
|
const text = (document.body?.innerText || '').toLowerCase();
|
|
126
191
|
const isResearching = text.includes('researching...') ||
|
|
127
192
|
text.includes('reading sources') ||
|
|
128
|
-
text.includes('
|
|
129
|
-
|
|
193
|
+
text.includes('正在研究') ||
|
|
194
|
+
text.includes('正在阅读来源');
|
|
195
|
+
return { isResearching };
|
|
130
196
|
})()`,
|
|
131
197
|
returnByValue: true,
|
|
132
198
|
});
|
|
133
199
|
const val = result?.value;
|
|
134
200
|
if (val?.isResearching) {
|
|
135
|
-
|
|
136
|
-
return;
|
|
201
|
+
return reportResearchStarted();
|
|
137
202
|
}
|
|
138
|
-
await delay(
|
|
203
|
+
await delay(2_000);
|
|
139
204
|
}
|
|
140
|
-
logger("
|
|
205
|
+
logger("[browser] Deep Research plan wait elapsed; proceeding to monitor research progress.");
|
|
206
|
+
return capturedPlan;
|
|
141
207
|
}
|
|
142
208
|
/**
|
|
143
209
|
* Polls for Deep Research completion over 5-30+ minutes.
|
|
@@ -600,6 +666,44 @@ function buildDeepResearchFrameStatusExpression() {
|
|
|
600
666
|
return `(() => {
|
|
601
667
|
const rawText = document.body?.innerText || '';
|
|
602
668
|
const html = document.body?.innerHTML || '';
|
|
669
|
+
const cleanText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
|
|
670
|
+
const sections = typeof document.querySelectorAll === 'function'
|
|
671
|
+
? Array.from(document.querySelectorAll('section'))
|
|
672
|
+
: [];
|
|
673
|
+
const planSection = sections.find((section) => {
|
|
674
|
+
if (typeof section.querySelector !== 'function' ||
|
|
675
|
+
typeof section.querySelectorAll !== 'function' ||
|
|
676
|
+
!cleanText(section.querySelector('h2')?.textContent) ||
|
|
677
|
+
section.querySelectorAll('ul li').length === 0) {
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
const buttons = Array.from(section.querySelectorAll('button'));
|
|
681
|
+
const hasPlanAction = buttons.some((button) =>
|
|
682
|
+
/^(edit|update|编辑|更新)$/i.test(cleanText(button.textContent))
|
|
683
|
+
);
|
|
684
|
+
const hasResearchStatus = Boolean(section.querySelector('p.loading-shimmer')) ||
|
|
685
|
+
buttons.some((button) =>
|
|
686
|
+
/stop research|停止研究/i.test(cleanText(button.getAttribute?.('aria-label')))
|
|
687
|
+
);
|
|
688
|
+
return hasPlanAction || hasResearchStatus;
|
|
689
|
+
});
|
|
690
|
+
const planTitle = cleanText(planSection?.querySelector?.('h2')?.textContent);
|
|
691
|
+
const planSteps = planSection && typeof planSection.querySelectorAll === 'function'
|
|
692
|
+
? Array.from(planSection.querySelectorAll('ul li'))
|
|
693
|
+
.map((item) => cleanText(item.textContent))
|
|
694
|
+
.filter(Boolean)
|
|
695
|
+
: [];
|
|
696
|
+
const planActionText = planSection && typeof planSection.querySelectorAll === 'function'
|
|
697
|
+
? Array.from(planSection.querySelectorAll('button'))
|
|
698
|
+
.map((button) => cleanText(button.textContent))
|
|
699
|
+
.find((text) => /^(edit|update|编辑|更新)$/i.test(text)) || ''
|
|
700
|
+
: '';
|
|
701
|
+
const hasResearchShimmer = Boolean(planSection?.querySelector?.('p.loading-shimmer'));
|
|
702
|
+
const hasStopResearchControl = typeof planSection?.querySelectorAll === 'function' &&
|
|
703
|
+
Array.from(planSection.querySelectorAll('button')).some((button) =>
|
|
704
|
+
/stop research|停止研究/i.test(cleanText(button.getAttribute?.('aria-label')))
|
|
705
|
+
);
|
|
706
|
+
const researchStarted = planSteps.length > 0 && (hasResearchShimmer || hasStopResearchControl);
|
|
603
707
|
const isPlaceholder = (line) => /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/i.test(line);
|
|
604
708
|
const isCompletionLine = (line) =>
|
|
605
709
|
/^(research completed|badanie ukończone)\\b/i.test(line);
|
|
@@ -641,6 +745,10 @@ function buildDeepResearchFrameStatusExpression() {
|
|
|
641
745
|
return {
|
|
642
746
|
completed,
|
|
643
747
|
inProgress,
|
|
748
|
+
researchStarted,
|
|
749
|
+
planTitle: planTitle || undefined,
|
|
750
|
+
planSteps: planSteps.length > 0 ? planSteps : undefined,
|
|
751
|
+
planActionText: planActionText || undefined,
|
|
644
752
|
textLength: reportText.length || rawText.trim().length,
|
|
645
753
|
text: completed ? reportText : undefined,
|
|
646
754
|
html: completed ? html : undefined,
|
|
@@ -773,9 +881,9 @@ export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex =
|
|
|
773
881
|
return buildDeepResearchCompletionPollExpression(minTurnIndex);
|
|
774
882
|
}
|
|
775
883
|
function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPill") {
|
|
776
|
-
const
|
|
884
|
+
const pillLabels = JSON.stringify([DEEP_RESEARCH_PILL_LABEL, "深度研究"]);
|
|
777
885
|
return `const ${functionName} = () => {
|
|
778
|
-
const
|
|
886
|
+
const labels = ${pillLabels}.map(label => label.toLowerCase());
|
|
779
887
|
const selectors = [
|
|
780
888
|
'.__composer-pill-composite',
|
|
781
889
|
'.__composer-pill',
|
|
@@ -798,7 +906,7 @@ function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPil
|
|
|
798
906
|
pill.querySelector('button')?.getAttribute('aria-label') ||
|
|
799
907
|
''
|
|
800
908
|
).toLowerCase();
|
|
801
|
-
if (text.includes(label) || aria.includes(label)) {
|
|
909
|
+
if (labels.some(label => text.includes(label) || aria.includes(label))) {
|
|
802
910
|
return pill;
|
|
803
911
|
}
|
|
804
912
|
}
|
|
@@ -819,6 +927,18 @@ function buildWaitForDeepResearchPillExpression(timeoutMs) {
|
|
|
819
927
|
function buildActivateDeepResearchExpression() {
|
|
820
928
|
const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
|
|
821
929
|
const targetText = JSON.stringify(DEEP_RESEARCH_DROPDOWN_ITEM_TEXT);
|
|
930
|
+
const targetLabels = JSON.stringify([DEEP_RESEARCH_DROPDOWN_ITEM_TEXT, "深度研究"]);
|
|
931
|
+
const descriptionLabels = JSON.stringify(["Get a detailed report", "获取详细报告"]);
|
|
932
|
+
const addFilesLabels = JSON.stringify(["add files", "添加文件"]);
|
|
933
|
+
const dropdownReadyLabels = JSON.stringify([
|
|
934
|
+
"add photos",
|
|
935
|
+
"create image",
|
|
936
|
+
"web search",
|
|
937
|
+
"deep research",
|
|
938
|
+
"get a detailed report",
|
|
939
|
+
"深度研究",
|
|
940
|
+
"获取详细报告",
|
|
941
|
+
]);
|
|
822
942
|
return `(async () => {
|
|
823
943
|
${buildClickDispatcher()}
|
|
824
944
|
${buildFindDeepResearchPillExpression()}
|
|
@@ -864,8 +984,12 @@ function buildActivateDeepResearchExpression() {
|
|
|
864
984
|
'[data-radix-popper-content-wrapper]',
|
|
865
985
|
'[data-floating-ui-portal]',
|
|
866
986
|
].join(',');
|
|
867
|
-
const target = ${targetText}.toLowerCase();
|
|
868
987
|
const normalizeText = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
|
|
988
|
+
const compactText = (value) => normalizeText(value).replace(/\\s+/g, '');
|
|
989
|
+
const targetLabels = ${targetLabels}.map(normalizeText);
|
|
990
|
+
const descriptionLabels = ${descriptionLabels}.map(normalizeText);
|
|
991
|
+
const addFilesLabels = ${addFilesLabels}.map(normalizeText);
|
|
992
|
+
const dropdownReadyLabels = ${dropdownReadyLabels}.map(normalizeText);
|
|
869
993
|
const getText = (item) => normalizeText(item.textContent || item.getAttribute?.('aria-label') || '');
|
|
870
994
|
const isInPopover = (item) => Boolean(item.closest?.(popoverSelector));
|
|
871
995
|
const isVisible = (item) => {
|
|
@@ -891,14 +1015,16 @@ function buildActivateDeepResearchExpression() {
|
|
|
891
1015
|
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
892
1016
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
893
1017
|
};
|
|
894
|
-
const isDeepResearchText = (text) =>
|
|
895
|
-
|
|
896
|
-
text.startsWith(
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1018
|
+
const isDeepResearchText = (text) => {
|
|
1019
|
+
const compact = compactText(text);
|
|
1020
|
+
const exactLabel = targetLabels.some(label => compact === compactText(label) || text.startsWith(label + ' '));
|
|
1021
|
+
const exactDescription = descriptionLabels.some(
|
|
1022
|
+
label => text === label || text.startsWith(label + ' ')
|
|
1023
|
+
);
|
|
1024
|
+
const combinedLabel = targetLabels.some(label => compact.startsWith(compactText(label))) &&
|
|
1025
|
+
descriptionLabels.some(label => compact.includes(compactText(label)));
|
|
1026
|
+
return exactLabel || exactDescription || combinedLabel;
|
|
1027
|
+
};
|
|
902
1028
|
const getClickableItem = (item) => item.closest?.(
|
|
903
1029
|
'[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"], .__menu-item, [class*="__menu-item"], [class*="menu-item"]'
|
|
904
1030
|
) || item;
|
|
@@ -915,7 +1041,7 @@ function buildActivateDeepResearchExpression() {
|
|
|
915
1041
|
.map(item => {
|
|
916
1042
|
const text = getText(item);
|
|
917
1043
|
const clickable = getClickableItem(item);
|
|
918
|
-
const exact = text
|
|
1044
|
+
const exact = targetLabels.includes(text) ? 0 : 1;
|
|
919
1045
|
const menuRow = /(^|\\s)__menu-item(\\s|$)/.test(clickable.className || '') ? 0 : 1;
|
|
920
1046
|
return { item: clickable, score: exact + menuRow, textLength: text.length };
|
|
921
1047
|
})
|
|
@@ -946,7 +1072,9 @@ function buildActivateDeepResearchExpression() {
|
|
|
946
1072
|
// mutate the main composer and can be submitted as normal prompt text.
|
|
947
1073
|
const plusBtn = document.querySelector(${plusBtnSelector}) ||
|
|
948
1074
|
Array.from(document.querySelectorAll('button')).find(
|
|
949
|
-
b =>
|
|
1075
|
+
b => addFilesLabels.some(label =>
|
|
1076
|
+
normalizeText(b.getAttribute('aria-label') || '').includes(label)
|
|
1077
|
+
)
|
|
950
1078
|
);
|
|
951
1079
|
if (!plusBtn) return { status: 'plus-button-missing' };
|
|
952
1080
|
dispatchClickSequence(plusBtn);
|
|
@@ -958,11 +1086,7 @@ function buildActivateDeepResearchExpression() {
|
|
|
958
1086
|
const items = collectAvailableItems({ requirePopover: true });
|
|
959
1087
|
if (findDeepResearchItem({ requirePopover: true }) || items.some(text => {
|
|
960
1088
|
const normalized = normalizeText(text);
|
|
961
|
-
return normalized.includes(
|
|
962
|
-
normalized.includes('create image') ||
|
|
963
|
-
normalized.includes('web search') ||
|
|
964
|
-
normalized.includes('deep research') ||
|
|
965
|
-
normalized.includes('get a detailed report');
|
|
1089
|
+
return dropdownReadyLabels.some(label => normalized.includes(label));
|
|
966
1090
|
})) { resolve(items); return; }
|
|
967
1091
|
elapsed += 150;
|
|
968
1092
|
if (elapsed > 3000) { resolve(items.length ? items : null); return; }
|
|
@@ -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;
|
|
@@ -1062,6 +1090,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1062
1090
|
if (seen.has(button) || !isVisible(button)) continue;
|
|
1063
1091
|
seen.add(button);
|
|
1064
1092
|
if (button.getAttribute?.('data-testid') === 'model-switcher-dropdown-button') continue;
|
|
1093
|
+
// A 5.6 Pro model pill is not Astra Latest's 6-prefixed effort owner.
|
|
1094
|
+
// Keep this rejection ahead of the generic compatibility matcher.
|
|
1095
|
+
if (isSolModelPillForLatest(button.textContent ?? '')) continue;
|
|
1065
1096
|
const label = normalize(
|
|
1066
1097
|
(button.getAttribute?.('aria-label') ?? '') + ' ' +
|
|
1067
1098
|
(button.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
@@ -1071,6 +1102,11 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1071
1102
|
(TARGET_MODEL_KIND === 'pro' && hasToken(label, 'pro') && !hasToken(label, 'thinking')) ||
|
|
1072
1103
|
(TARGET_MODEL_KIND === 'thinking' && hasToken(label, 'thinking') && !hasToken(label, 'pro')) ||
|
|
1073
1104
|
(!TARGET_MODEL_KIND && hasToken(label, 'thinking')) ||
|
|
1105
|
+
// Astra Latest prefixes a supported effort label with "6" (for
|
|
1106
|
+
// example, "6 Pro" or textContent-concatenated "6Pro"). This is
|
|
1107
|
+
// recognized only for the exact Latest target; selection still
|
|
1108
|
+
// requires the direct slider's leading label and numeric ARIA proof.
|
|
1109
|
+
isAstraLatestEffortPill(button.textContent ?? '') ||
|
|
1074
1110
|
(button.matches?.('button.__composer-pill') && matchesAnyEffortLevel(label))
|
|
1075
1111
|
) {
|
|
1076
1112
|
return button;
|
|
@@ -1157,6 +1193,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
1157
1193
|
tokens.some((token) => normalize(token) === pillLabel),
|
|
1158
1194
|
);
|
|
1159
1195
|
const pillNamesEffortNotModel =
|
|
1196
|
+
TARGET_IS_ASTRA_LATEST ||
|
|
1160
1197
|
TARGET_IS_GPT56_MODEL ||
|
|
1161
1198
|
(pillIsBareEffortTier && Boolean(document.querySelector(INTELLIGENCE_MENU_SELECTOR)));
|
|
1162
1199
|
const composerModelKind =
|