@steipete/oracle 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +1 -1
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +235 -121
- package/dist/src/browser/actions/attachments.js +3 -4
- package/dist/src/browser/actions/deepResearch.js +6 -6
- package/dist/src/browser/actions/modelSelection.js +133 -22
- package/dist/src/browser/actions/navigation.js +235 -14
- package/dist/src/browser/actions/promptComposer.js +4 -5
- package/dist/src/browser/actions/thinkingStatus.js +228 -0
- package/dist/src/browser/actions/thinkingTime.js +68 -11
- package/dist/src/browser/chatgptFiles.js +4 -7
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +30 -28
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/constants.js +6 -1
- package/dist/src/browser/controlPlan.js +2 -2
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +121 -72
- package/dist/src/browser/liveTabs.js +113 -31
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/projectSourcesRunner.js +7 -6
- package/dist/src/browser/reattach.js +29 -13
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/browser/recoverConversation.js +90 -29
- package/dist/src/cli/browserConfig.js +5 -1
- package/dist/src/cli/browserTabs.js +54 -33
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/runOptions.js +6 -1
- package/dist/src/cli/sessionDisplay.js +38 -14
- package/dist/src/oracle/config.js +25 -0
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/oscProgress.js +3 -2
- 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 +21 -22
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -569
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -416
- package/dist/docs-site/browser-mode.html +0 -594
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -462
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -375
- package/dist/docs-site/gemini.html +0 -383
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -596
- package/dist/docs-site/mcp.html +0 -391
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -399
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -61,6 +61,7 @@ export function resolveBrowserConfig(config) {
|
|
|
61
61
|
const debugPortEnv = parseDebugPort(process.env.ORACLE_BROWSER_PORT ?? process.env.ORACLE_BROWSER_DEBUG_PORT);
|
|
62
62
|
const envAllowCookieErrors = (process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? "").trim().toLowerCase() === "true" ||
|
|
63
63
|
(process.env.ORACLE_BROWSER_ALLOW_COOKIE_ERRORS ?? "").trim() === "1";
|
|
64
|
+
const envMaxConcurrentTabs = parseMaxConcurrentTabs(process.env.ORACLE_BROWSER_MAX_CONCURRENT_TABS);
|
|
64
65
|
const rawUrl = config?.chatgptUrl ?? config?.url ?? DEFAULT_BROWSER_CONFIG.url;
|
|
65
66
|
const normalizedUrl = normalizeChatgptUrl(rawUrl ?? DEFAULT_BROWSER_CONFIG.url, DEFAULT_BROWSER_CONFIG.url);
|
|
66
67
|
const desiredModel = config?.desiredModel ?? DEFAULT_BROWSER_CONFIG.desiredModel ?? DEFAULT_MODEL_TARGET;
|
|
@@ -87,7 +88,7 @@ export function resolveBrowserConfig(config) {
|
|
|
87
88
|
assistantRecheckTimeoutMs: config?.assistantRecheckTimeoutMs ?? DEFAULT_BROWSER_CONFIG.assistantRecheckTimeoutMs,
|
|
88
89
|
reuseChromeWaitMs: config?.reuseChromeWaitMs ?? DEFAULT_BROWSER_CONFIG.reuseChromeWaitMs,
|
|
89
90
|
profileLockTimeoutMs: config?.profileLockTimeoutMs ?? DEFAULT_BROWSER_CONFIG.profileLockTimeoutMs,
|
|
90
|
-
maxConcurrentTabs: normalizeMaxConcurrentTabs(config?.maxConcurrentTabs ?? DEFAULT_BROWSER_CONFIG.maxConcurrentTabs),
|
|
91
|
+
maxConcurrentTabs: normalizeMaxConcurrentTabs(config?.maxConcurrentTabs ?? envMaxConcurrentTabs ?? DEFAULT_BROWSER_CONFIG.maxConcurrentTabs),
|
|
91
92
|
autoReattachDelayMs: config?.autoReattachDelayMs ?? DEFAULT_BROWSER_CONFIG.autoReattachDelayMs,
|
|
92
93
|
autoReattachIntervalMs: config?.autoReattachIntervalMs ?? DEFAULT_BROWSER_CONFIG.autoReattachIntervalMs,
|
|
93
94
|
autoReattachTimeoutMs: config?.autoReattachTimeoutMs ?? DEFAULT_BROWSER_CONFIG.autoReattachTimeoutMs,
|
|
@@ -134,6 +135,18 @@ function parseDebugPort(raw) {
|
|
|
134
135
|
}
|
|
135
136
|
return value;
|
|
136
137
|
}
|
|
138
|
+
function parseMaxConcurrentTabs(raw) {
|
|
139
|
+
if (!raw)
|
|
140
|
+
return null;
|
|
141
|
+
const trimmed = raw.trim();
|
|
142
|
+
if (!/^\d+$/.test(trimmed))
|
|
143
|
+
return null;
|
|
144
|
+
const value = Number(trimmed);
|
|
145
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
137
150
|
function resolveManualLoginProfileDir(...candidates) {
|
|
138
151
|
for (const candidate of candidates) {
|
|
139
152
|
const profileDir = candidate?.trim();
|
|
@@ -32,6 +32,7 @@ export const ANSWER_SELECTORS = [
|
|
|
32
32
|
export const CONVERSATION_TURN_SELECTOR = 'article[data-testid^="conversation-turn"], div[data-testid^="conversation-turn"], section[data-testid^="conversation-turn"], ' +
|
|
33
33
|
"article[data-message-author-role], div[data-message-author-role], section[data-message-author-role], " +
|
|
34
34
|
"article[data-turn], div[data-turn], section[data-turn]";
|
|
35
|
+
export const CONVERSATION_TURN_CONTAINER_SELECTOR = '[data-testid^="conversation-turn"]';
|
|
35
36
|
export const ASSISTANT_ROLE_SELECTOR = '[data-message-author-role="assistant"], [data-turn="assistant"]';
|
|
36
37
|
export const CLOUDFLARE_SCRIPT_SELECTOR = 'script[src*="/challenge-platform/"]';
|
|
37
38
|
export const CLOUDFLARE_TITLE = "just a moment";
|
|
@@ -63,10 +64,14 @@ export const UPLOAD_STATUS_SELECTORS = [
|
|
|
63
64
|
'[aria-live="assertive"]',
|
|
64
65
|
];
|
|
65
66
|
export const STOP_BUTTON_SELECTOR = '[data-testid="stop-button"]';
|
|
67
|
+
// The aria-label fallback exists for data-testid drift, but a document-wide match makes ANY
|
|
68
|
+
// visible "stop" control (read-aloud, voice/dictation) read as "still generating", which blocks
|
|
69
|
+
// completion until the response timeout. Scope it to the composer form and exclude the known
|
|
70
|
+
// non-generation stop controls that legitimately live there.
|
|
66
71
|
export const STOP_BUTTON_SELECTORS = [
|
|
67
72
|
STOP_BUTTON_SELECTOR,
|
|
68
73
|
'[data-testid="composer-stop-button"]',
|
|
69
|
-
'button[aria-label*="stop" i]',
|
|
74
|
+
'form button[aria-label*="stop" i]:not([aria-label*="dictat" i]):not([aria-label*="voice" i]):not([aria-label*="read" i])',
|
|
70
75
|
];
|
|
71
76
|
export const SEND_BUTTON_SELECTORS = [
|
|
72
77
|
'button[data-testid="send-button"]',
|
|
@@ -45,13 +45,13 @@ export function describeBrowserControlPlan(config = {}) {
|
|
|
45
45
|
};
|
|
46
46
|
}
|
|
47
47
|
if (config.hideWindow) {
|
|
48
|
-
guidance.push("
|
|
48
|
+
guidance.push("On macOS, Oracle launches Chrome off-screen while keeping the page rendered.");
|
|
49
49
|
guidance.push("For the calmest shared-desktop flow, prefer --browser-attach-running or --remote-chrome.");
|
|
50
50
|
return {
|
|
51
51
|
mode: "hidden-window",
|
|
52
52
|
launchesChrome: true,
|
|
53
53
|
mayFocusWindow: true,
|
|
54
|
-
summary: "launch Chrome
|
|
54
|
+
summary: "launch Chrome in hidden-window mode",
|
|
55
55
|
guidance,
|
|
56
56
|
};
|
|
57
57
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { CONVERSATION_TURN_CONTAINER_SELECTOR, CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
/** Build a browser-context expression that returns one DOM node per conversation turn. */
|
|
3
|
+
export function buildConversationTurnListExpression(rootExpression = "document") {
|
|
4
|
+
const containerSelector = JSON.stringify(CONVERSATION_TURN_CONTAINER_SELECTOR);
|
|
5
|
+
const fallbackSelector = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
6
|
+
return `(() => {
|
|
7
|
+
const root = ${rootExpression};
|
|
8
|
+
const containers = Array.from(root.querySelectorAll(${containerSelector}));
|
|
9
|
+
return containers.length > 0
|
|
10
|
+
? containers
|
|
11
|
+
: Array.from(root.querySelectorAll(${fallbackSelector}));
|
|
12
|
+
})()`;
|
|
13
|
+
}
|
|
14
|
+
export function buildConversationTurnCountExpression(rootExpression = "document") {
|
|
15
|
+
return `(${buildConversationTurnListExpression(rootExpression)}).length`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { delay } from "./utils.js";
|
|
2
|
+
export function createConversationUrlMonitor(options) {
|
|
3
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
4
|
+
const wait = options.wait ?? delay;
|
|
5
|
+
const now = options.now ?? Date.now;
|
|
6
|
+
let inFlight = null;
|
|
7
|
+
let stopped = false;
|
|
8
|
+
const activePersists = new Set();
|
|
9
|
+
const update = async (label, timeoutMs = 10_000) => {
|
|
10
|
+
const startedAt = now();
|
|
11
|
+
while (!stopped && now() - startedAt < timeoutMs) {
|
|
12
|
+
try {
|
|
13
|
+
const url = await options.readUrl();
|
|
14
|
+
if (stopped) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
if (url && isConversationUrl(url)) {
|
|
18
|
+
options.logger(`[browser] conversation url (${label}) = ${url}`);
|
|
19
|
+
const persist = options.persistUrl(url);
|
|
20
|
+
activePersists.add(persist);
|
|
21
|
+
try {
|
|
22
|
+
await persist;
|
|
23
|
+
}
|
|
24
|
+
finally {
|
|
25
|
+
activePersists.delete(persist);
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// The page can navigate or disconnect between polls; keep trying until timeout.
|
|
32
|
+
}
|
|
33
|
+
await wait(pollIntervalMs);
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
const schedule = (label, timeoutMs) => {
|
|
38
|
+
if (stopped) {
|
|
39
|
+
return Promise.resolve(false);
|
|
40
|
+
}
|
|
41
|
+
if (inFlight) {
|
|
42
|
+
return inFlight;
|
|
43
|
+
}
|
|
44
|
+
// The /c/ URL can appear after submit. Persist it without blocking response capture.
|
|
45
|
+
inFlight = update(label, timeoutMs)
|
|
46
|
+
.catch(() => false)
|
|
47
|
+
.finally(() => {
|
|
48
|
+
inFlight = null;
|
|
49
|
+
});
|
|
50
|
+
return inFlight;
|
|
51
|
+
};
|
|
52
|
+
return {
|
|
53
|
+
update,
|
|
54
|
+
schedule,
|
|
55
|
+
isInFlight: () => inFlight !== null,
|
|
56
|
+
stop: async () => {
|
|
57
|
+
stopped = true;
|
|
58
|
+
await Promise.allSettled(activePersists);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function isConversationUrl(url) {
|
|
63
|
+
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
64
|
+
}
|
|
@@ -3,6 +3,56 @@ import { delay } from "./utils.js";
|
|
|
3
3
|
import { getCookies } from "@steipete/sweet-cookie";
|
|
4
4
|
export class ChromeCookieSyncError extends Error {
|
|
5
5
|
}
|
|
6
|
+
export async function clearStaleChatGptConversationCookies(Network, Target, logger, options = {}) {
|
|
7
|
+
try {
|
|
8
|
+
const preservedNames = new Set((options.preserveConversationIds ?? [])
|
|
9
|
+
.filter((id) => Boolean(id))
|
|
10
|
+
.map((id) => `conv_key_${id}`));
|
|
11
|
+
try {
|
|
12
|
+
const { targetInfos = [] } = await Target.getTargets();
|
|
13
|
+
for (const target of targetInfos) {
|
|
14
|
+
const conversationId = extractChatGptConversationId(target.url ?? "");
|
|
15
|
+
if (conversationId) {
|
|
16
|
+
preservedNames.add(`conv_key_${conversationId}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
logger(`[cookies] Failed to inspect active ChatGPT conversations; skipping stale cookie cleanup: ${message}`);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const { cookies = [] } = await Network.getAllCookies();
|
|
26
|
+
const targets = cookies.filter((cookie) => isChatGptConversationCookie(cookie) && !preservedNames.has(String(cookie.name ?? "")));
|
|
27
|
+
if (targets.length === 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
let deleted = 0;
|
|
31
|
+
for (const cookie of targets) {
|
|
32
|
+
try {
|
|
33
|
+
await Network.deleteCookies({
|
|
34
|
+
name: cookie.name,
|
|
35
|
+
domain: cookie.domain,
|
|
36
|
+
path: cookie.path ?? "/",
|
|
37
|
+
});
|
|
38
|
+
deleted += 1;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
42
|
+
logger(`[cookies] Failed to clear a stale ChatGPT conversation cookie: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (deleted > 0) {
|
|
46
|
+
logger(`[cookies] Cleared ${deleted} stale ChatGPT conversation cookie${deleted === 1 ? "" : "s"}.`);
|
|
47
|
+
}
|
|
48
|
+
return deleted;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
logger(`[cookies] Failed to inspect ChatGPT conversation cookies: ${message}`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
6
56
|
export async function syncCookies(Network, url, profile, logger, options = {}) {
|
|
7
57
|
const { allowErrors = false, filterNames, inlineCookies, cookiePath, waitMs = 0 } = options;
|
|
8
58
|
try {
|
|
@@ -94,6 +144,28 @@ async function readChromeCookies(url, profile, filterNames, cookiePath) {
|
|
|
94
144
|
}
|
|
95
145
|
return Array.from(merged.values());
|
|
96
146
|
}
|
|
147
|
+
function isChatGptConversationCookie(cookie) {
|
|
148
|
+
if (!cookie.name?.startsWith("conv_key_")) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const domain = String(cookie.domain ?? "")
|
|
152
|
+
.replace(/^\./, "")
|
|
153
|
+
.toLowerCase();
|
|
154
|
+
return domain === "chatgpt.com" || domain === "chat.openai.com";
|
|
155
|
+
}
|
|
156
|
+
function extractChatGptConversationId(url) {
|
|
157
|
+
try {
|
|
158
|
+
const parsed = new URL(url);
|
|
159
|
+
const domain = parsed.hostname.toLowerCase();
|
|
160
|
+
if (domain !== "chatgpt.com" && domain !== "chat.openai.com") {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
return parsed.pathname.match(/\/c\/([a-zA-Z0-9-]+)/)?.[1];
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
97
169
|
function normalizeInlineCookies(rawCookies, fallbackHost) {
|
|
98
170
|
const merged = new Map();
|
|
99
171
|
for (const cookie of rawCookies) {
|
|
@@ -4,9 +4,9 @@ import os from "node:os";
|
|
|
4
4
|
import net from "node:net";
|
|
5
5
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
6
|
import { copyChromeProfile } from "./profileCopy.js";
|
|
7
|
-
import { launchChrome, registerTerminationHooks,
|
|
8
|
-
import { syncCookies } from "./cookies.js";
|
|
9
|
-
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
7
|
+
import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, connectToRemoteChrome, connectWithNewTab, closeTab, closeRemoteChromeTarget, closeBlankChromeTabs, } from "./chromeLifecycle.js";
|
|
8
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
|
+
import { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, ensureModelSelection, clearPromptComposer, waitForAssistantResponse, captureAssistantMarkdown, clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, readAssistantSnapshot, } from "./pageActions.js";
|
|
10
10
|
import { INPUT_SELECTORS } from "./constants.js";
|
|
11
11
|
import { uploadAttachmentViaDataTransfer } from "./actions/remoteFileTransfer.js";
|
|
12
12
|
import { ensureThinkingTime } from "./actions/thinkingTime.js";
|
|
@@ -14,9 +14,10 @@ import { startThinkingStatusMonitor } from "./actions/thinkingStatus.js";
|
|
|
14
14
|
import { activateDeepResearch, captureDeepResearchTargetKeys, waitForDeepResearchCompletion, waitForResearchPlanAutoConfirm, } from "./actions/deepResearch.js";
|
|
15
15
|
import { estimateTokenCount, withRetries, delay } from "./utils.js";
|
|
16
16
|
import { formatElapsed } from "../oracle/format.js";
|
|
17
|
-
import { CHATGPT_URL,
|
|
17
|
+
import { CHATGPT_URL, DEFAULT_MODEL_STRATEGY } from "./constants.js";
|
|
18
18
|
import { BrowserAutomationError } from "../oracle/errors.js";
|
|
19
19
|
import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
|
|
20
|
+
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
20
21
|
import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
21
22
|
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
22
23
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
@@ -30,6 +31,7 @@ import { captureBrowserDiagnostics } from "./domDebug.js";
|
|
|
30
31
|
import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./actions/archiveConversation.js";
|
|
31
32
|
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
32
33
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
|
|
34
|
+
import { createConversationUrlMonitor, } from "./conversationUrlMonitor.js";
|
|
33
35
|
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
|
|
34
36
|
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
|
|
35
37
|
export { formatThinkingLog, formatThinkingWaitingLog, buildThinkingStatusExpressionForTest, readThinkingStatusForTest, sanitizeThinkingText, startThinkingStatusMonitorForTest, } from "./actions/thinkingStatus.js";
|
|
@@ -279,6 +281,25 @@ async function createAssistantTimeoutError(params) {
|
|
|
279
281
|
}
|
|
280
282
|
return warningError;
|
|
281
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Make the page behave like a focused foreground tab.
|
|
286
|
+
*
|
|
287
|
+
* The send button is activated with trusted CDP input events dispatched at
|
|
288
|
+
* viewport coordinates. Chrome delivers those only to a window that is being
|
|
289
|
+
* composited, so a hidden (`--browser-hide-window`), minimized, or occluded
|
|
290
|
+
* window swallows the click while the automation still believes it clicked.
|
|
291
|
+
* Soft-fails: focus emulation is an optimization, never a hard requirement.
|
|
292
|
+
*/
|
|
293
|
+
async function enableFocusEmulation(client, logger, label) {
|
|
294
|
+
try {
|
|
295
|
+
await client.Emulation.setFocusEmulationEnabled({ enabled: true });
|
|
296
|
+
logger(`[browser] Focus emulation enabled for ${label}`);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
300
|
+
logger(`[browser] Focus emulation unavailable: ${message}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
282
303
|
function listIgnoredRemoteChromeFlags(config) {
|
|
283
304
|
return [
|
|
284
305
|
config.headless ? "--browser-headless" : null,
|
|
@@ -354,13 +375,13 @@ async function pollGeneratedImageOrTextAssistantResponse(Runtime, timeoutMs, min
|
|
|
354
375
|
}
|
|
355
376
|
return null;
|
|
356
377
|
}
|
|
357
|
-
function isImageOnlyUiChromeText(text) {
|
|
378
|
+
export function isImageOnlyUiChromeText(text) {
|
|
358
379
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
359
380
|
return (normalized.length === 0 ||
|
|
360
381
|
normalized === "edit" ||
|
|
361
382
|
normalized === "stopped thinking" ||
|
|
362
383
|
normalized === "stopped thinking edit" ||
|
|
363
|
-
/^thought for \d+(?:\.\d+)?\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\s+edit$/.test(normalized));
|
|
384
|
+
/^(?:reasoning\s+|pro thinking\s+)?thought for \d+(?:\.\d+)?\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\s+edit$/.test(normalized));
|
|
364
385
|
}
|
|
365
386
|
function normalizeBrowserFollowUpPrompts(values) {
|
|
366
387
|
return (values ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -560,6 +581,7 @@ export async function runBrowserMode(options) {
|
|
|
560
581
|
let promptSubmitted = false;
|
|
561
582
|
let modelSelectionEvidence;
|
|
562
583
|
let tabLease = null;
|
|
584
|
+
let conversationUrlMonitor = null;
|
|
563
585
|
const emitRuntimeHint = async () => {
|
|
564
586
|
if (!chrome?.port) {
|
|
565
587
|
return;
|
|
@@ -596,6 +618,7 @@ export async function runBrowserMode(options) {
|
|
|
596
618
|
}
|
|
597
619
|
promptSubmitted = true;
|
|
598
620
|
await emitRuntimeHint();
|
|
621
|
+
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
599
622
|
};
|
|
600
623
|
if (config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") {
|
|
601
624
|
logger(`[browser-mode] config: ${JSON.stringify({
|
|
@@ -746,7 +769,7 @@ export async function runBrowserMode(options) {
|
|
|
746
769
|
else {
|
|
747
770
|
const strictTabIsolation = Boolean(manualLogin && reusedChrome);
|
|
748
771
|
const devtoolsRetries = manualLogin ? 6 : 0;
|
|
749
|
-
const connection = await connectWithNewTab(chrome.port, logger,
|
|
772
|
+
const connection = await connectWithNewTab(chrome.port, logger, "about:blank", chromeHost, {
|
|
750
773
|
fallbackToDefault: !strictTabIsolation,
|
|
751
774
|
retries: devtoolsRetries,
|
|
752
775
|
retryDelayMs: 500,
|
|
@@ -778,15 +801,19 @@ export async function runBrowserMode(options) {
|
|
|
778
801
|
});
|
|
779
802
|
});
|
|
780
803
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
781
|
-
const { Network, Page, Runtime, Input, DOM } = client;
|
|
782
|
-
if (!config.headless && config.hideWindow) {
|
|
783
|
-
await hideChromeWindow(chrome, logger);
|
|
784
|
-
}
|
|
804
|
+
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
785
805
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
786
806
|
if (DOM && typeof DOM.enable === "function") {
|
|
787
807
|
domainEnablers.push(DOM.enable());
|
|
788
808
|
}
|
|
789
809
|
await Promise.all(domainEnablers);
|
|
810
|
+
if (!config.headless && config.hideWindow) {
|
|
811
|
+
await positionChromeWindowOffscreen(client, logger);
|
|
812
|
+
}
|
|
813
|
+
// The send button is clicked with trusted CDP input events at viewport
|
|
814
|
+
// coordinates, which ChatGPT silently drops when the window is hidden or
|
|
815
|
+
// occluded. Emulate focus so the page behaves like a foreground tab.
|
|
816
|
+
await enableFocusEmulation(client, logger, "local target");
|
|
790
817
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
791
818
|
if (!profileIsPreSigned) {
|
|
792
819
|
await Network.clearBrowserCookies();
|
|
@@ -828,6 +855,12 @@ export async function runBrowserMode(options) {
|
|
|
828
855
|
? "Skipping Chrome cookie sync (--browser-manual-login enabled); reuse the opened profile after signing in."
|
|
829
856
|
: "Skipping Chrome cookie sync (--browser-no-cookie-sync)");
|
|
830
857
|
}
|
|
858
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
859
|
+
preserveConversationIds: [
|
|
860
|
+
extractConversationIdFromUrl(config.resumeConversationUrl ?? ""),
|
|
861
|
+
extractConversationIdFromUrl(lastUrl ?? ""),
|
|
862
|
+
],
|
|
863
|
+
});
|
|
831
864
|
if (cookieSyncEnabled && !manualLogin && (appliedCookies ?? 0) === 0 && !config.inlineCookies) {
|
|
832
865
|
// Learned: if the profile has no ChatGPT cookies, browser mode will just bounce to login.
|
|
833
866
|
// Fail early so the user knows to sign in.
|
|
@@ -901,6 +934,18 @@ export async function runBrowserMode(options) {
|
|
|
901
934
|
}));
|
|
902
935
|
}
|
|
903
936
|
}
|
|
937
|
+
const chatMode = await raceWithDisconnect(ensureChatMode(Runtime, Input, config.inputTimeoutMs, logger, {
|
|
938
|
+
resetWorkConversation: config.browserTabRef && !isResumingConversation
|
|
939
|
+
? async () => {
|
|
940
|
+
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
941
|
+
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
942
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
943
|
+
}
|
|
944
|
+
: undefined,
|
|
945
|
+
}));
|
|
946
|
+
if (chatMode === "switched") {
|
|
947
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
948
|
+
}
|
|
904
949
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
905
950
|
const captureRuntimeSnapshot = async () => {
|
|
906
951
|
try {
|
|
@@ -939,44 +984,22 @@ export async function runBrowserMode(options) {
|
|
|
939
984
|
await emitRuntimeHint();
|
|
940
985
|
}
|
|
941
986
|
};
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
return true;
|
|
959
|
-
}
|
|
960
|
-
}
|
|
961
|
-
catch {
|
|
962
|
-
// ignore; keep polling until timeout
|
|
963
|
-
}
|
|
964
|
-
await delay(250);
|
|
965
|
-
}
|
|
966
|
-
return false;
|
|
967
|
-
};
|
|
968
|
-
const scheduleConversationHint = (label, timeoutMs) => {
|
|
969
|
-
if (conversationHintInFlight) {
|
|
970
|
-
return;
|
|
971
|
-
}
|
|
972
|
-
// Learned: the /c/ URL can update after the answer; emit hints in the background.
|
|
973
|
-
// Run in the background so prompt submission/streaming isn't blocked by slow URL updates.
|
|
974
|
-
conversationHintInFlight = updateConversationHint(label, timeoutMs)
|
|
975
|
-
.catch(() => false)
|
|
976
|
-
.finally(() => {
|
|
977
|
-
conversationHintInFlight = null;
|
|
978
|
-
});
|
|
979
|
-
};
|
|
987
|
+
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
988
|
+
readUrl: async () => {
|
|
989
|
+
const { result } = await Runtime.evaluate({
|
|
990
|
+
expression: "location.href",
|
|
991
|
+
returnByValue: true,
|
|
992
|
+
});
|
|
993
|
+
return typeof result?.value === "string" ? result.value : null;
|
|
994
|
+
},
|
|
995
|
+
persistUrl: async (url) => {
|
|
996
|
+
lastUrl = url;
|
|
997
|
+
await emitRuntimeHint();
|
|
998
|
+
},
|
|
999
|
+
logger,
|
|
1000
|
+
});
|
|
1001
|
+
conversationUrlMonitor = activeConversationUrlMonitor;
|
|
1002
|
+
const updateConversationHint = conversationUrlMonitor.update;
|
|
980
1003
|
await captureRuntimeSnapshot();
|
|
981
1004
|
const modelStrategy = config.modelStrategy ?? DEFAULT_MODEL_STRATEGY;
|
|
982
1005
|
if (config.desiredModel && modelStrategy !== "ignore" && !isResumingConversation) {
|
|
@@ -1128,8 +1151,6 @@ export async function runBrowserMode(options) {
|
|
|
1128
1151
|
}
|
|
1129
1152
|
}
|
|
1130
1153
|
}
|
|
1131
|
-
// Reattach needs a /c/ URL; ChatGPT can update it late, so poll in the background.
|
|
1132
|
-
scheduleConversationHint("post-submit", config.timeoutMs ?? 120_000);
|
|
1133
1154
|
return {
|
|
1134
1155
|
baselineTurns,
|
|
1135
1156
|
baselineAssistantText,
|
|
@@ -1725,6 +1746,7 @@ export async function runBrowserMode(options) {
|
|
|
1725
1746
|
}, normalizedError);
|
|
1726
1747
|
}
|
|
1727
1748
|
finally {
|
|
1749
|
+
await conversationUrlMonitor?.stop();
|
|
1728
1750
|
try {
|
|
1729
1751
|
if (!connectionClosedUnexpectedly) {
|
|
1730
1752
|
await client?.close();
|
|
@@ -1974,7 +1996,7 @@ function detachKeptChromeProcess(chrome) {
|
|
|
1974
1996
|
// Best-effort only; cleanup should not mask the original browser result.
|
|
1975
1997
|
}
|
|
1976
1998
|
}
|
|
1977
|
-
async function acquireManualLoginChromeForRun(userDataDir, config, logger, sessionId, deps = {}) {
|
|
1999
|
+
export async function acquireManualLoginChromeForRun(userDataDir, config, logger, sessionId, deps = {}) {
|
|
1978
2000
|
const maybeReuse = deps.maybeReuse ?? maybeReuseRunningChrome;
|
|
1979
2001
|
const launch = deps.launch ?? launchChrome;
|
|
1980
2002
|
const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
@@ -2086,6 +2108,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2086
2108
|
let modelSelectionEvidence;
|
|
2087
2109
|
let attachedExistingTab = false;
|
|
2088
2110
|
let ownsTarget = true;
|
|
2111
|
+
let conversationUrlMonitor = null;
|
|
2089
2112
|
const runtimeHintCb = options.runtimeHintCb;
|
|
2090
2113
|
const emitRuntimeHint = async () => {
|
|
2091
2114
|
if (!runtimeHintCb)
|
|
@@ -2120,6 +2143,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2120
2143
|
}
|
|
2121
2144
|
promptSubmitted = true;
|
|
2122
2145
|
await emitRuntimeHint();
|
|
2146
|
+
void conversationUrlMonitor?.schedule("post-submit", config.timeoutMs ?? 120_000);
|
|
2123
2147
|
};
|
|
2124
2148
|
const startedAt = Date.now();
|
|
2125
2149
|
let answerText = "";
|
|
@@ -2161,7 +2185,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2161
2185
|
logger(`Attached to existing remote ChatGPT tab ${attached.targetId}${attached.tab.url ? ` (${attached.tab.url})` : ""}`);
|
|
2162
2186
|
}
|
|
2163
2187
|
else {
|
|
2164
|
-
connection = await connectToRemoteChrome(host, port, logger,
|
|
2188
|
+
connection = await connectToRemoteChrome(host, port, logger, "about:blank", browserWSEndpoint, {
|
|
2165
2189
|
approvalWaitMs: config.attachRunning && browserWSEndpoint ? 20_000 : undefined,
|
|
2166
2190
|
});
|
|
2167
2191
|
client = connection.client;
|
|
@@ -2180,15 +2204,37 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2180
2204
|
connectionClosedUnexpectedly = true;
|
|
2181
2205
|
};
|
|
2182
2206
|
client.on("disconnect", markConnectionLost);
|
|
2183
|
-
const { Network, Page, Runtime, Input, DOM } = client;
|
|
2207
|
+
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
2184
2208
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
2185
2209
|
if (DOM && typeof DOM.enable === "function") {
|
|
2186
2210
|
domainEnablers.push(DOM.enable());
|
|
2187
2211
|
}
|
|
2188
2212
|
await Promise.all(domainEnablers);
|
|
2189
2213
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
2214
|
+
await enableFocusEmulation(client, logger, "remote target");
|
|
2215
|
+
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
2216
|
+
readUrl: async () => {
|
|
2217
|
+
const { result } = await Runtime.evaluate({
|
|
2218
|
+
expression: "location.href",
|
|
2219
|
+
returnByValue: true,
|
|
2220
|
+
});
|
|
2221
|
+
return typeof result?.value === "string" ? result.value : null;
|
|
2222
|
+
},
|
|
2223
|
+
persistUrl: async (url) => {
|
|
2224
|
+
lastUrl = url;
|
|
2225
|
+
await emitRuntimeHint();
|
|
2226
|
+
},
|
|
2227
|
+
logger,
|
|
2228
|
+
});
|
|
2229
|
+
conversationUrlMonitor = activeConversationUrlMonitor;
|
|
2190
2230
|
// Skip cookie sync for remote Chrome - it already has cookies
|
|
2191
2231
|
logger("Skipping cookie sync for remote Chrome (using existing session)");
|
|
2232
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
2233
|
+
preserveConversationIds: [
|
|
2234
|
+
extractConversationIdFromUrl(config.resumeConversationUrl ?? ""),
|
|
2235
|
+
extractConversationIdFromUrl(lastUrl ?? ""),
|
|
2236
|
+
],
|
|
2237
|
+
});
|
|
2192
2238
|
if (config.resumeConversationUrl) {
|
|
2193
2239
|
await navigateToChatGPT(Page, Runtime, config.resumeConversationUrl, logger);
|
|
2194
2240
|
}
|
|
@@ -2204,6 +2250,18 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2204
2250
|
expectedConversationUrl: config.resumeConversationUrl,
|
|
2205
2251
|
});
|
|
2206
2252
|
}
|
|
2253
|
+
const chatMode = await ensureChatMode(Runtime, Input, config.inputTimeoutMs, logger, {
|
|
2254
|
+
resetWorkConversation: attachedExistingTab && !config.resumeConversationUrl
|
|
2255
|
+
? async () => {
|
|
2256
|
+
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
2257
|
+
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
2258
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2259
|
+
}
|
|
2260
|
+
: undefined,
|
|
2261
|
+
});
|
|
2262
|
+
if (chatMode === "switched") {
|
|
2263
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2264
|
+
}
|
|
2207
2265
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
2208
2266
|
try {
|
|
2209
2267
|
const { result } = await Runtime.evaluate({
|
|
@@ -2361,7 +2419,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2361
2419
|
ignoredTargetKeys: deepResearchTargetKeys,
|
|
2362
2420
|
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
2363
2421
|
});
|
|
2364
|
-
await
|
|
2422
|
+
await activeConversationUrlMonitor.update("post-deep-research", 15_000).catch(() => false);
|
|
2365
2423
|
const durationMs = Date.now() - startedAt;
|
|
2366
2424
|
const tokens = estimateTokenCount(researchResult.text);
|
|
2367
2425
|
const reportArtifact = await saveOptionalArtifact(() => saveDeepResearchReportArtifact({
|
|
@@ -2512,11 +2570,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2512
2570
|
const captureAssistantTurn = async (turnPrompt, label) => {
|
|
2513
2571
|
let turnAnswer;
|
|
2514
2572
|
try {
|
|
2515
|
-
|
|
2516
|
-
if (conversationUrl && isConversationUrl(conversationUrl)) {
|
|
2517
|
-
lastUrl = conversationUrl;
|
|
2518
|
-
await emitRuntimeHint();
|
|
2519
|
-
}
|
|
2573
|
+
await activeConversationUrlMonitor.update("assistant-wait", 15_000).catch(() => false);
|
|
2520
2574
|
turnAnswer = await waitWithThinkingMonitor(() => waitForAssistantOrGeneratedImageResponse({
|
|
2521
2575
|
Runtime,
|
|
2522
2576
|
waitForText: () => waitForAssistantResponseWithReload(Runtime, Page, config.timeoutMs, logger, baselineTurns ?? undefined, expectedConversationId()),
|
|
@@ -2534,16 +2588,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2534
2588
|
turnAnswer = rechecked;
|
|
2535
2589
|
}
|
|
2536
2590
|
else {
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
lastUrl = conversationUrl;
|
|
2541
|
-
}
|
|
2542
|
-
}
|
|
2543
|
-
catch {
|
|
2544
|
-
// ignore
|
|
2545
|
-
}
|
|
2546
|
-
await emitRuntimeHint();
|
|
2591
|
+
await activeConversationUrlMonitor
|
|
2592
|
+
.update("assistant-timeout", 15_000)
|
|
2593
|
+
.catch(() => false);
|
|
2547
2594
|
const diagnostics = await captureBrowserDiagnostics(Runtime, logger, "assistant-timeout", {
|
|
2548
2595
|
Page,
|
|
2549
2596
|
sessionId: options.sessionId,
|
|
@@ -2572,6 +2619,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2572
2619
|
throw error;
|
|
2573
2620
|
}
|
|
2574
2621
|
}
|
|
2622
|
+
await activeConversationUrlMonitor.update("post-response", 15_000).catch(() => false);
|
|
2575
2623
|
const baselineNormalized = baselineAssistantText
|
|
2576
2624
|
? normalizeForComparison(baselineAssistantText)
|
|
2577
2625
|
: "";
|
|
@@ -2836,6 +2884,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2836
2884
|
});
|
|
2837
2885
|
}
|
|
2838
2886
|
finally {
|
|
2887
|
+
await conversationUrlMonitor?.stop();
|
|
2839
2888
|
try {
|
|
2840
2889
|
await closeRemoteConnectionAfterRun({
|
|
2841
2890
|
connectionClosedUnexpectedly,
|
|
@@ -3053,12 +3102,12 @@ function buildSessionValidationExpression() {
|
|
|
3053
3102
|
})()`;
|
|
3054
3103
|
}
|
|
3055
3104
|
async function readConversationTurnCount(Runtime, logger) {
|
|
3056
|
-
const
|
|
3105
|
+
const expression = buildConversationTurnCountExpression();
|
|
3057
3106
|
const attempts = 4;
|
|
3058
3107
|
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
3059
3108
|
try {
|
|
3060
3109
|
const { result } = await Runtime.evaluate({
|
|
3061
|
-
expression
|
|
3110
|
+
expression,
|
|
3062
3111
|
returnByValue: true,
|
|
3063
3112
|
});
|
|
3064
3113
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|