@steipete/oracle 0.18.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/README.md +1 -1
- package/dist/bin/oracle-cli.js +76 -76
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/archiveConversation.js +31 -7
- package/dist/src/browser/actions/assistantResponse.js +43 -1
- package/dist/src/browser/actions/attachmentContext.js +239 -0
- package/dist/src/browser/actions/attachmentDataTransfer.js +19 -1
- package/dist/src/browser/actions/attachmentEvidence.js +78 -0
- package/dist/src/browser/actions/attachmentProgress.js +41 -0
- package/dist/src/browser/actions/attachmentPrompt.js +130 -0
- package/dist/src/browser/actions/attachments.js +306 -355
- package/dist/src/browser/actions/deepResearch.js +170 -45
- package/dist/src/browser/actions/modelSelection.js +91 -4
- package/dist/src/browser/actions/navigation.js +1 -1
- package/dist/src/browser/actions/promptComposer.js +364 -186
- package/dist/src/browser/actions/remoteFileTransfer.js +4 -2
- package/dist/src/browser/actions/thinkingStatus.js +2 -5
- package/dist/src/browser/actions/thinkingTime.js +156 -16
- package/dist/src/browser/actions/webSearch.js +96 -0
- package/dist/src/browser/attachRunning.js +46 -2
- package/dist/src/browser/attachmentValidation.js +53 -0
- package/dist/src/browser/cancellation.js +105 -0
- package/dist/src/browser/chatgptImages.js +2 -0
- package/dist/src/browser/chatgptThrottle.js +104 -0
- package/dist/src/browser/chromeLifecycle.js +272 -47
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/detect.js +1 -1
- package/dist/src/browser/index.js +525 -274
- package/dist/src/browser/liveTabs.js +3 -4
- package/dist/src/browser/modelDisplay.js +9 -0
- package/dist/src/browser/pageActions.js +2 -2
- package/dist/src/browser/profileState.js +114 -9
- package/dist/src/browser/projectSourcesRunner.js +83 -15
- package/dist/src/browser/prompt.js +272 -113
- package/dist/src/browser/providers/chatgptDomProvider.js +3 -0
- package/dist/src/browser/reattach.js +31 -4
- package/dist/src/browser/recoveryTarget.js +131 -0
- package/dist/src/browser/sessionRunner.js +73 -11
- package/dist/src/browser/tabLeaseRegistry.js +434 -38
- package/dist/src/browser/targetClaim.js +54 -0
- package/dist/src/browser/utils.js +20 -2
- package/dist/src/cli/browserConfig.js +36 -4
- package/dist/src/cli/browserDefaults.js +10 -0
- package/dist/src/cli/browserTabs.js +15 -29
- package/dist/src/cli/detachedSession.js +69 -0
- package/dist/src/cli/dryRun.js +58 -48
- package/dist/src/cli/harvestIntegrity.js +140 -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/serveBrowserConfig.js +24 -0
- package/dist/src/cli/sessionDisplay.js +13 -7
- package/dist/src/cli/sessionRunner.js +95 -12
- package/dist/src/config.js +1 -0
- package/dist/src/gemini-web/client.js +67 -21
- package/dist/src/gemini-web/executor.js +4 -0
- package/dist/src/gemini-web/http.js +15 -0
- package/dist/src/gemini-web/models.js +15 -11
- package/dist/src/mcp/server.js +11 -14
- package/dist/src/mcp/tools/chatgptImage.js +4 -4
- package/dist/src/mcp/tools/consult.js +86 -24
- package/dist/src/mcp/tools/projectSources.js +4 -6
- package/dist/src/mcp/tools/sessionResources.js +1 -1
- package/dist/src/mcp/tools/sessions.js +2 -2
- package/dist/src/mcp/tools/wait.js +182 -0
- package/dist/src/mcp/types.js +8 -1
- package/dist/src/oracle/config.js +14 -0
- package/dist/src/oracle/errors.js +11 -0
- package/dist/src/oracle/geminiModels.js +1 -0
- package/dist/src/oracle/providerRoutePlan.js +3 -2
- package/dist/src/oracle/run.js +27 -4
- package/dist/src/remote/client.js +73 -6
- package/dist/src/remote/health.js +6 -2
- package/dist/src/remote/runSlots.js +83 -0
- package/dist/src/remote/server.js +346 -71
- package/dist/src/sessionManager.js +36 -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 +25 -23
- 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 -683
- 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 -400
- package/dist/docs-site/browser-mode.html +0 -588
- 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 -436
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -368
- package/dist/docs-site/gemini.html +0 -379
- 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 -595
- package/dist/docs-site/mcp.html +0 -368
- 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 -387
- 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/markdansi/types/index.js +0 -4
- package/dist/oracle/bin/oracle-cli.js +0 -472
- package/dist/oracle/src/browser/actions/assistantResponse.js +0 -471
- package/dist/oracle/src/browser/actions/attachments.js +0 -82
- package/dist/oracle/src/browser/actions/modelSelection.js +0 -190
- package/dist/oracle/src/browser/actions/navigation.js +0 -75
- package/dist/oracle/src/browser/actions/promptComposer.js +0 -167
- package/dist/oracle/src/browser/chromeLifecycle.js +0 -104
- package/dist/oracle/src/browser/config.js +0 -33
- package/dist/oracle/src/browser/constants.js +0 -40
- package/dist/oracle/src/browser/cookies.js +0 -210
- package/dist/oracle/src/browser/domDebug.js +0 -36
- package/dist/oracle/src/browser/index.js +0 -331
- package/dist/oracle/src/browser/pageActions.js +0 -5
- package/dist/oracle/src/browser/prompt.js +0 -88
- package/dist/oracle/src/browser/promptSummary.js +0 -20
- package/dist/oracle/src/browser/sessionRunner.js +0 -80
- package/dist/oracle/src/browser/types.js +0 -1
- package/dist/oracle/src/browser/utils.js +0 -62
- package/dist/oracle/src/browserMode.js +0 -1
- package/dist/oracle/src/cli/browserConfig.js +0 -44
- package/dist/oracle/src/cli/dryRun.js +0 -59
- package/dist/oracle/src/cli/engine.js +0 -17
- package/dist/oracle/src/cli/errorUtils.js +0 -9
- package/dist/oracle/src/cli/help.js +0 -70
- package/dist/oracle/src/cli/markdownRenderer.js +0 -15
- package/dist/oracle/src/cli/options.js +0 -103
- package/dist/oracle/src/cli/promptRequirement.js +0 -14
- package/dist/oracle/src/cli/rootAlias.js +0 -30
- package/dist/oracle/src/cli/sessionCommand.js +0 -77
- package/dist/oracle/src/cli/sessionDisplay.js +0 -270
- package/dist/oracle/src/cli/sessionRunner.js +0 -94
- package/dist/oracle/src/heartbeat.js +0 -43
- package/dist/oracle/src/oracle/client.js +0 -48
- package/dist/oracle/src/oracle/config.js +0 -29
- package/dist/oracle/src/oracle/errors.js +0 -101
- package/dist/oracle/src/oracle/files.js +0 -220
- package/dist/oracle/src/oracle/format.js +0 -33
- package/dist/oracle/src/oracle/fsAdapter.js +0 -7
- package/dist/oracle/src/oracle/oscProgress.js +0 -60
- package/dist/oracle/src/oracle/request.js +0 -48
- package/dist/oracle/src/oracle/run.js +0 -444
- package/dist/oracle/src/oracle/tokenStats.js +0 -39
- package/dist/oracle/src/oracle/types.js +0 -1
- package/dist/oracle/src/oracle.js +0 -9
- package/dist/oracle/src/sessionManager.js +0 -205
- package/dist/oracle/src/version.js +0 -39
- package/dist/scripts/chrome/browser-tools.js +0 -295
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/profileSync.js +0 -141
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -3,7 +3,7 @@ import { buildConversationTurnListExpression } from "../conversationTurns.js";
|
|
|
3
3
|
import { delay } from "../utils.js";
|
|
4
4
|
import { isDeepResearchIncompleteText } from "../deepResearchResult.js";
|
|
5
5
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
6
|
-
import { captureAssistantMarkdown, readAssistantSnapshot } from "./assistantResponse.js";
|
|
6
|
+
import { captureAssistantMarkdown, readAssistantSnapshot, throwIfAssistantUiError, } from "./assistantResponse.js";
|
|
7
7
|
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
8
8
|
/**
|
|
9
9
|
* Activates Deep Research mode through ChatGPT's composer tools menu and
|
|
@@ -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.
|
|
@@ -248,6 +314,7 @@ export async function waitForDeepResearchCompletion(Runtime, logger, timeoutMs =
|
|
|
248
314
|
*/
|
|
249
315
|
export async function extractDeepResearchResult(Runtime, logger, minTurnIndex) {
|
|
250
316
|
const snapshot = await readAssistantSnapshot(Runtime, minTurnIndex);
|
|
317
|
+
throwIfAssistantUiError(snapshot);
|
|
251
318
|
const meta = {
|
|
252
319
|
turnId: snapshot?.turnId ?? null,
|
|
253
320
|
messageId: snapshot?.messageId ?? null,
|
|
@@ -599,6 +666,44 @@ function buildDeepResearchFrameStatusExpression() {
|
|
|
599
666
|
return `(() => {
|
|
600
667
|
const rawText = document.body?.innerText || '';
|
|
601
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);
|
|
602
707
|
const isPlaceholder = (line) => /^(called tool|used tool|użyto narzędzia|narzędzie wywołane)$/i.test(line);
|
|
603
708
|
const isCompletionLine = (line) =>
|
|
604
709
|
/^(research completed|badanie ukończone)\\b/i.test(line);
|
|
@@ -640,6 +745,10 @@ function buildDeepResearchFrameStatusExpression() {
|
|
|
640
745
|
return {
|
|
641
746
|
completed,
|
|
642
747
|
inProgress,
|
|
748
|
+
researchStarted,
|
|
749
|
+
planTitle: planTitle || undefined,
|
|
750
|
+
planSteps: planSteps.length > 0 ? planSteps : undefined,
|
|
751
|
+
planActionText: planActionText || undefined,
|
|
643
752
|
textLength: reportText.length || rawText.trim().length,
|
|
644
753
|
text: completed ? reportText : undefined,
|
|
645
754
|
html: completed ? html : undefined,
|
|
@@ -772,9 +881,9 @@ export function buildDeepResearchCompletionPollExpressionForTest(minTurnIndex =
|
|
|
772
881
|
return buildDeepResearchCompletionPollExpression(minTurnIndex);
|
|
773
882
|
}
|
|
774
883
|
function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPill") {
|
|
775
|
-
const
|
|
884
|
+
const pillLabels = JSON.stringify([DEEP_RESEARCH_PILL_LABEL, "深度研究"]);
|
|
776
885
|
return `const ${functionName} = () => {
|
|
777
|
-
const
|
|
886
|
+
const labels = ${pillLabels}.map(label => label.toLowerCase());
|
|
778
887
|
const selectors = [
|
|
779
888
|
'.__composer-pill-composite',
|
|
780
889
|
'.__composer-pill',
|
|
@@ -797,7 +906,7 @@ function buildFindDeepResearchPillExpression(functionName = "findDeepResearchPil
|
|
|
797
906
|
pill.querySelector('button')?.getAttribute('aria-label') ||
|
|
798
907
|
''
|
|
799
908
|
).toLowerCase();
|
|
800
|
-
if (text.includes(label) || aria.includes(label)) {
|
|
909
|
+
if (labels.some(label => text.includes(label) || aria.includes(label))) {
|
|
801
910
|
return pill;
|
|
802
911
|
}
|
|
803
912
|
}
|
|
@@ -818,6 +927,18 @@ function buildWaitForDeepResearchPillExpression(timeoutMs) {
|
|
|
818
927
|
function buildActivateDeepResearchExpression() {
|
|
819
928
|
const plusBtnSelector = JSON.stringify(DEEP_RESEARCH_PLUS_BUTTON);
|
|
820
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
|
+
]);
|
|
821
942
|
return `(async () => {
|
|
822
943
|
${buildClickDispatcher()}
|
|
823
944
|
${buildFindDeepResearchPillExpression()}
|
|
@@ -863,8 +984,12 @@ function buildActivateDeepResearchExpression() {
|
|
|
863
984
|
'[data-radix-popper-content-wrapper]',
|
|
864
985
|
'[data-floating-ui-portal]',
|
|
865
986
|
].join(',');
|
|
866
|
-
const target = ${targetText}.toLowerCase();
|
|
867
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);
|
|
868
993
|
const getText = (item) => normalizeText(item.textContent || item.getAttribute?.('aria-label') || '');
|
|
869
994
|
const isInPopover = (item) => Boolean(item.closest?.(popoverSelector));
|
|
870
995
|
const isVisible = (item) => {
|
|
@@ -890,14 +1015,16 @@ function buildActivateDeepResearchExpression() {
|
|
|
890
1015
|
input.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text }));
|
|
891
1016
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
892
1017
|
};
|
|
893
|
-
const isDeepResearchText = (text) =>
|
|
894
|
-
|
|
895
|
-
text.startsWith(
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
+
};
|
|
901
1028
|
const getClickableItem = (item) => item.closest?.(
|
|
902
1029
|
'[data-radix-collection-item], [role="option"], [cmdk-item], button, [role="menuitem"], [role="menuitemradio"], .__menu-item, [class*="__menu-item"], [class*="menu-item"]'
|
|
903
1030
|
) || item;
|
|
@@ -914,7 +1041,7 @@ function buildActivateDeepResearchExpression() {
|
|
|
914
1041
|
.map(item => {
|
|
915
1042
|
const text = getText(item);
|
|
916
1043
|
const clickable = getClickableItem(item);
|
|
917
|
-
const exact = text
|
|
1044
|
+
const exact = targetLabels.includes(text) ? 0 : 1;
|
|
918
1045
|
const menuRow = /(^|\\s)__menu-item(\\s|$)/.test(clickable.className || '') ? 0 : 1;
|
|
919
1046
|
return { item: clickable, score: exact + menuRow, textLength: text.length };
|
|
920
1047
|
})
|
|
@@ -945,7 +1072,9 @@ function buildActivateDeepResearchExpression() {
|
|
|
945
1072
|
// mutate the main composer and can be submitted as normal prompt text.
|
|
946
1073
|
const plusBtn = document.querySelector(${plusBtnSelector}) ||
|
|
947
1074
|
Array.from(document.querySelectorAll('button')).find(
|
|
948
|
-
b =>
|
|
1075
|
+
b => addFilesLabels.some(label =>
|
|
1076
|
+
normalizeText(b.getAttribute('aria-label') || '').includes(label)
|
|
1077
|
+
)
|
|
949
1078
|
);
|
|
950
1079
|
if (!plusBtn) return { status: 'plus-button-missing' };
|
|
951
1080
|
dispatchClickSequence(plusBtn);
|
|
@@ -957,11 +1086,7 @@ function buildActivateDeepResearchExpression() {
|
|
|
957
1086
|
const items = collectAvailableItems({ requirePopover: true });
|
|
958
1087
|
if (findDeepResearchItem({ requirePopover: true }) || items.some(text => {
|
|
959
1088
|
const normalized = normalizeText(text);
|
|
960
|
-
return normalized.includes(
|
|
961
|
-
normalized.includes('create image') ||
|
|
962
|
-
normalized.includes('web search') ||
|
|
963
|
-
normalized.includes('deep research') ||
|
|
964
|
-
normalized.includes('get a detailed report');
|
|
1089
|
+
return dropdownReadyLabels.some(label => normalized.includes(label));
|
|
965
1090
|
})) { resolve(items); return; }
|
|
966
1091
|
elapsed += 150;
|
|
967
1092
|
if (elapsed > 3000) { resolve(items.length ? items : null); return; }
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { COMPOSER_MODEL_SIGNAL_SELECTOR, MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
2
|
import { logDomFailure } from "../domDebug.js";
|
|
3
3
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
4
|
+
import { throwIfThrottled } from "../chatgptThrottle.js";
|
|
4
5
|
import { delay } from "../utils.js";
|
|
5
6
|
const LEGACY_PRO_VERSION_WORD_TOKENS = ["5 4", "5 2", "5 1", "5 0", "gpt 5 pro"];
|
|
6
7
|
const LEGACY_PRO_VERSION_COMPACT_TOKENS = ["gpt54", "gpt52", "gpt51", "gpt50"];
|
|
@@ -55,6 +56,11 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
55
56
|
}
|
|
56
57
|
case "option-not-found": {
|
|
57
58
|
await logDomFailure(Runtime, logger, "model-switcher-option");
|
|
59
|
+
// Check for a rate-limit notice before blaming the model name. The notice
|
|
60
|
+
// is a plain dialog, so its "Got it" button reads as an available option to
|
|
61
|
+
// the menu scrape — which turns "wait a few minutes" into "your model does
|
|
62
|
+
// not exist", and sends the reader after the wrong bug.
|
|
63
|
+
await throwIfThrottled(Runtime, { stage: "model-selection" }, logger);
|
|
58
64
|
const isTemporary = result.hint?.temporaryChat ?? false;
|
|
59
65
|
const available = (result.hint?.availableOptions ?? []).filter(Boolean);
|
|
60
66
|
const availableHint = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
|
|
@@ -65,6 +71,7 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
65
71
|
}
|
|
66
72
|
default: {
|
|
67
73
|
await logDomFailure(Runtime, logger, "model-switcher-button");
|
|
74
|
+
await throwIfThrottled(Runtime, { stage: "model-selection" }, logger);
|
|
68
75
|
throw new Error("Unable to locate the ChatGPT model selector button. If the desired model is already selected in the browser, retry with --browser-model-strategy current; otherwise retry with --browser-model-strategy ignore to skip model selection.");
|
|
69
76
|
}
|
|
70
77
|
}
|
|
@@ -74,6 +81,15 @@ function assertResolvedModelSelection(desiredModel, resolvedLabel) {
|
|
|
74
81
|
const resolved = resolvedLabel.toLowerCase();
|
|
75
82
|
const normalizedDesired = normalizeResolvedModelLabel(desired);
|
|
76
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
|
+
}
|
|
77
93
|
const wantsGpt56Sol = /(?:^| )5 6(?: |$)/.test(normalizedDesired) && normalizedDesired.split(" ").includes("sol");
|
|
78
94
|
if (wantsGpt56Sol) {
|
|
79
95
|
const resolvedTokens = normalizedResolved.split(" ");
|
|
@@ -162,6 +178,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
162
178
|
const hasToken = (value, token) => normalizeText(value).split(' ').includes(token);
|
|
163
179
|
// Normalize every candidate token to keep fuzzy matching deterministic.
|
|
164
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
|
+
};
|
|
165
192
|
const normalizedTokens = Array.from(new Set([normalizedTarget, ...LABEL_TOKENS]))
|
|
166
193
|
.map((token) => normalizeText(token))
|
|
167
194
|
.filter(Boolean);
|
|
@@ -303,6 +330,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
303
330
|
};
|
|
304
331
|
|
|
305
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
|
+
};
|
|
306
341
|
const getComposerModelLabel = () =>
|
|
307
342
|
(document.querySelector(COMPOSER_MODEL_SIGNAL_SELECTOR)?.textContent ?? '').trim();
|
|
308
343
|
const readComposerModelSignal = () => normalizeText(getComposerModelLabel());
|
|
@@ -406,10 +441,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
406
441
|
const ADVANCED_VIEW_SELECTOR = '[data-testid="composer-model-picker-slider-advanced-view"]';
|
|
407
442
|
const INTELLIGENCE_PICKER_SELECTOR = '[data-testid="composer-intelligence-picker-content"]';
|
|
408
443
|
const SUBMENU_OPENER_SELECTOR = '[role="menuitem"][aria-haspopup="menu"]';
|
|
409
|
-
const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', 'avanzado', 'avancado', 'avance'];
|
|
410
|
-
const MODEL_WORDS = ['model', 'modell', '模型', 'modelo', 'modello', 'modele'];
|
|
444
|
+
const ADVANCED_WORDS = ['advanced', 'erweitert', '高级', '고급', 'avanzado', 'avancado', 'avance'];
|
|
445
|
+
const MODEL_WORDS = ['model', 'modell', '模型', '모델', 'modelo', 'modello', 'modele'];
|
|
411
446
|
const EFFORT_WORDS = [
|
|
412
|
-
'effort', 'aufwand', '强度', '努力',
|
|
447
|
+
'effort', 'aufwand', '强度', '努力', '추론 수준',
|
|
413
448
|
'esfuerzo', 'esforco', 'sforzo', 'inspanning', 'wysilek',
|
|
414
449
|
];
|
|
415
450
|
const pickerNodeLabel = (node) => {
|
|
@@ -420,6 +455,7 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
420
455
|
.toLowerCase()
|
|
421
456
|
.normalize('NFD')
|
|
422
457
|
.replace(/[\u0300-\u036f]/g, '')
|
|
458
|
+
.normalize('NFC')
|
|
423
459
|
.replace(/\\s+/g, ' ')
|
|
424
460
|
.trim();
|
|
425
461
|
} catch {
|
|
@@ -473,15 +509,37 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
473
509
|
if (wantsInstant) return label.includes('instant');
|
|
474
510
|
if (wantsThinking) return Boolean(desiredVersion) && !labelHasProWord(label);
|
|
475
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
|
+
}
|
|
476
522
|
return normalizedTokens.some((token) => token && label.includes(token));
|
|
477
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
|
+
};
|
|
478
532
|
const getAdvancedModelLabel = () => {
|
|
479
533
|
const opener = findModelSubmenuOpener(findUnifiedPickerMenu());
|
|
480
534
|
if (!opener) return '';
|
|
481
535
|
const raw = (opener.textContent ?? '').trim();
|
|
482
536
|
const normalized = normalizeText(pickerNodeLabel(opener));
|
|
483
537
|
const version = versionFromLabel(normalized);
|
|
484
|
-
if (!version)
|
|
538
|
+
if (!version) {
|
|
539
|
+
const checkedAdvancedRadio = findCheckedAdvancedModelRadio(findUnifiedPickerMenu());
|
|
540
|
+
const checkedLabel = (checkedAdvancedRadio?.textContent ?? '').trim();
|
|
541
|
+
return checkedLabel || raw;
|
|
542
|
+
}
|
|
485
543
|
const [major, minor] = version.split('-');
|
|
486
544
|
const suffix = normalized.split(' ').includes('sol') ? ' Sol' : '';
|
|
487
545
|
return 'GPT-' + major + '.' + minor + suffix;
|
|
@@ -495,6 +553,17 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
495
553
|
);
|
|
496
554
|
};
|
|
497
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
|
+
}
|
|
498
567
|
if (configuredSelectionMatchesTarget()) {
|
|
499
568
|
const variant = getConfiguredVariantLabel();
|
|
500
569
|
const version = formatModelOptionLabel(getConfiguredVersionLabel());
|
|
@@ -636,6 +705,13 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
636
705
|
return COMPOSER_SIGNAL_INCLUDES.some((token) => token && signal.includes(token));
|
|
637
706
|
};
|
|
638
707
|
const activeSelectionMatchesTarget = () => {
|
|
708
|
+
if (targetIsLatest) {
|
|
709
|
+
const checkedAdvancedRadio = findCheckedAdvancedModelRadio();
|
|
710
|
+
if (checkedAdvancedRadio) {
|
|
711
|
+
return isLatestModelLabel(checkedAdvancedRadio.textContent ?? '');
|
|
712
|
+
}
|
|
713
|
+
return latestButtonSelected();
|
|
714
|
+
}
|
|
639
715
|
if (advancedModelSignalMatchesTarget()) {
|
|
640
716
|
return true;
|
|
641
717
|
}
|
|
@@ -686,6 +762,8 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
686
762
|
node.getAttribute('data-composer-intelligence-pro-effort-action') === 'true' ||
|
|
687
763
|
Boolean(node.closest('[data-model-picker-thinking-effort-action="true"]')) ||
|
|
688
764
|
Boolean(node.closest('[data-composer-intelligence-pro-effort-action="true"]')) ||
|
|
765
|
+
(isUnifiedPickerMenu(menu) && isSubmenuOpener(node) &&
|
|
766
|
+
containsPickerWord(pickerNodeLabel(node), EFFORT_WORDS)) ||
|
|
689
767
|
isDetachedProEffortMenu(menu));
|
|
690
768
|
const optionIsSelected = (node) => {
|
|
691
769
|
if (!(node instanceof HTMLElement)) {
|
|
@@ -708,6 +786,11 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
708
786
|
|
|
709
787
|
const scoreOption = (normalizedText, testid, node) => {
|
|
710
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
|
+
}
|
|
711
794
|
if (!normalizedText && !testid) {
|
|
712
795
|
return 0;
|
|
713
796
|
}
|
|
@@ -1358,6 +1441,10 @@ function buildModelMatchersLiteral(targetModel) {
|
|
|
1358
1441
|
testIdTokens.add("gpt5-6");
|
|
1359
1442
|
testIdTokens.add("gpt56");
|
|
1360
1443
|
}
|
|
1444
|
+
if (base === "latest") {
|
|
1445
|
+
// ChatGPT's Japanese advanced-model radio is named exactly "最新".
|
|
1446
|
+
push("最新", labelTokens);
|
|
1447
|
+
}
|
|
1361
1448
|
// Numeric variations (5.5 <-> 55 <-> gpt-5-5)
|
|
1362
1449
|
if (base.includes("5.5") || base.includes("5-5") || base.includes("55")) {
|
|
1363
1450
|
push("5.5", labelTokens);
|
|
@@ -483,7 +483,7 @@ export async function ensurePromptReady(Runtime, timeoutMs, logger) {
|
|
|
483
483
|
throw new Error("Prompt textarea did not appear before timeout");
|
|
484
484
|
}
|
|
485
485
|
}
|
|
486
|
-
function conversationIdFromUrl(value) {
|
|
486
|
+
export function conversationIdFromUrl(value) {
|
|
487
487
|
if (!value)
|
|
488
488
|
return null;
|
|
489
489
|
try {
|