@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { BrowserAutomationError } from "../../oracle/errors.js";
|
|
2
|
+
import { INPUT_SELECTORS } from "../constants.js";
|
|
3
|
+
import { delay } from "../utils.js";
|
|
4
|
+
import { activateComposerPlus, captureComposerNavigationUrl, assertComposerPlusStayedInPlace, } from "./attachments.js";
|
|
5
|
+
import { buildComposerNavigationValidationExpression } from "./attachmentContext.js";
|
|
6
|
+
import { buildClickDispatcher } from "./domEvents.js";
|
|
7
|
+
export function matchesWebSearchMenuLabel(value) {
|
|
8
|
+
return [
|
|
9
|
+
"search",
|
|
10
|
+
"searchfindontheweb",
|
|
11
|
+
"websearch",
|
|
12
|
+
"websearchfindreal-timenewsandinfo",
|
|
13
|
+
].includes(value.replace(/\s+/g, "").toLowerCase());
|
|
14
|
+
}
|
|
15
|
+
export function buildWebSearchVerificationExpression(prompt) {
|
|
16
|
+
return `(() => {
|
|
17
|
+
const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
|
|
18
|
+
const editor = ${JSON.stringify(INPUT_SELECTORS)}.flatMap(selector => Array.from(document.querySelectorAll(selector))).find(visible);
|
|
19
|
+
if (!editor) return { selected: false, promptMatches: false };
|
|
20
|
+
const chip = editor.querySelector('[data-inline-selection-pill][data-id="search"][data-system-hint-type="search"]');
|
|
21
|
+
const copy = editor.cloneNode(true);
|
|
22
|
+
copy.querySelectorAll('[data-inline-selection-pill], [data-inline-selection-pill-cursor-target]').forEach(node => node.remove());
|
|
23
|
+
const readText = node => {
|
|
24
|
+
if (node.nodeType === 3) return node.textContent ?? '';
|
|
25
|
+
const text = Array.from(node.childNodes).map(readText).join('');
|
|
26
|
+
return text + (['P', 'DIV', 'BR', 'LI', 'PRE'].includes(node.nodeName) ? '\\n' : '');
|
|
27
|
+
};
|
|
28
|
+
const normalize = text => String(text ?? '').replace(/[\\u200b\\ufeff]/g, '').replace(/\\s+/g, ' ').trim();
|
|
29
|
+
return { selected: Boolean(chip && visible(chip)), promptMatches: normalize(readText(copy)) === normalize(${JSON.stringify(prompt)}) };
|
|
30
|
+
})()`;
|
|
31
|
+
}
|
|
32
|
+
export function buildWebSearchSelectionExpression(navigationUrl) {
|
|
33
|
+
return `(() => {
|
|
34
|
+
${buildClickDispatcher()}
|
|
35
|
+
const matchesLabel = ${matchesWebSearchMenuLabel.toString()};
|
|
36
|
+
const navigation = ${buildComposerNavigationValidationExpression(navigationUrl)};
|
|
37
|
+
if (!navigation.contextMatches || navigation.workSelected || navigation.modeUnverified) return 'context-changed';
|
|
38
|
+
const visible = node => node instanceof HTMLElement && node.getBoundingClientRect().width > 0 && node.getBoundingClientRect().height > 0;
|
|
39
|
+
const roots = Array.from(document.querySelectorAll('main .popover, [data-radix-popper-content-wrapper], [data-floating-ui-portal], [role="menu"], [role="listbox"]')).filter(visible);
|
|
40
|
+
const candidates = roots.flatMap(root => Array.from(root.querySelectorAll('[data-radix-collection-item], [role="menuitem"], [role="option"], .__menu-item, [class*="menu-item"]')));
|
|
41
|
+
const match = candidates.find(node => {
|
|
42
|
+
if (!visible(node) || node.hasAttribute('disabled') || node.getAttribute('aria-disabled') === 'true') return false;
|
|
43
|
+
return matchesLabel(node.textContent ?? '');
|
|
44
|
+
});
|
|
45
|
+
if (!match) return 'missing';
|
|
46
|
+
dispatchClickSequence(match);
|
|
47
|
+
return 'clicked';
|
|
48
|
+
})()`;
|
|
49
|
+
}
|
|
50
|
+
/** Web Search is an inline editor hint, so activate it after staging text/attachments. */
|
|
51
|
+
export async function activateWebSearch(runtime, input, prompt, logger) {
|
|
52
|
+
const navigationUrl = await captureComposerNavigationUrl(runtime);
|
|
53
|
+
const verify = async () => {
|
|
54
|
+
const { result, exceptionDetails } = await runtime.evaluate({
|
|
55
|
+
expression: buildWebSearchVerificationExpression(prompt),
|
|
56
|
+
returnByValue: true,
|
|
57
|
+
});
|
|
58
|
+
if (exceptionDetails)
|
|
59
|
+
return false;
|
|
60
|
+
return result?.value?.selected === true && result?.value?.promptMatches === true;
|
|
61
|
+
};
|
|
62
|
+
if (await verify())
|
|
63
|
+
return;
|
|
64
|
+
const activated = await activateComposerPlus(runtime, input, navigationUrl);
|
|
65
|
+
if (activated.method === "unavailable")
|
|
66
|
+
throw new BrowserAutomationError("Web Search requires the ChatGPT composer tools menu.", {
|
|
67
|
+
stage: "web-search-activate",
|
|
68
|
+
});
|
|
69
|
+
const deadline = Date.now() + 5_000;
|
|
70
|
+
let clicked = false;
|
|
71
|
+
while (Date.now() < deadline) {
|
|
72
|
+
const outcome = await runtime.evaluate({
|
|
73
|
+
expression: buildWebSearchSelectionExpression(navigationUrl),
|
|
74
|
+
returnByValue: true,
|
|
75
|
+
});
|
|
76
|
+
if (outcome.result?.value === "clicked") {
|
|
77
|
+
clicked = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
if (outcome.exceptionDetails || outcome.result?.value !== "missing")
|
|
81
|
+
break;
|
|
82
|
+
await delay(100);
|
|
83
|
+
}
|
|
84
|
+
if (clicked) {
|
|
85
|
+
const confirmationDeadline = Date.now() + 3_000;
|
|
86
|
+
do {
|
|
87
|
+
await assertComposerPlusStayedInPlace(runtime, navigationUrl);
|
|
88
|
+
if (await verify()) {
|
|
89
|
+
logger("Web Search selected; inline search hint and staged prompt verified.");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
await delay(100);
|
|
93
|
+
} while (Date.now() < confirmationDeadline);
|
|
94
|
+
}
|
|
95
|
+
throw new BrowserAutomationError("Web Search selection could not be verified; the prompt was not submitted. This pilot supports the English ChatGPT Web search control.", { stage: "web-search-activate" });
|
|
96
|
+
}
|
|
@@ -352,9 +352,10 @@ export async function connectToRemoteChrome(host, port, logger, targetUrl, brows
|
|
|
352
352
|
return {
|
|
353
353
|
client: targetConnection.client,
|
|
354
354
|
targetId: targetConnection.targetId,
|
|
355
|
-
close: async () => {
|
|
355
|
+
close: async (closeOptions) => {
|
|
356
356
|
await targetConnection.client.close().catch(() => undefined);
|
|
357
|
-
|
|
357
|
+
if (!closeOptions?.preserveTarget)
|
|
358
|
+
await closeRemoteChromeTarget(host, port, targetConnection.targetId, logger);
|
|
358
359
|
},
|
|
359
360
|
};
|
|
360
361
|
}
|
|
@@ -431,9 +432,9 @@ export async function connectToRemoteChromeTarget(host, port, logger, options) {
|
|
|
431
432
|
client,
|
|
432
433
|
targetId,
|
|
433
434
|
browserWSEndpoint: options.browserWSEndpoint,
|
|
434
|
-
close: async () => {
|
|
435
|
+
close: async (closeOptions) => {
|
|
435
436
|
await browser.Target.detachFromTarget({ sessionId: attached.sessionId }).catch(() => undefined);
|
|
436
|
-
if (options.closeTargetOnDispose && targetId) {
|
|
437
|
+
if (options.closeTargetOnDispose && targetId && !closeOptions?.preserveTarget) {
|
|
437
438
|
await browser.Target.closeTarget({ targetId }).catch(() => undefined);
|
|
438
439
|
}
|
|
439
440
|
await browser.close().catch(() => undefined);
|
|
@@ -123,7 +123,7 @@ export function resolveBrowserConfig(config) {
|
|
|
123
123
|
};
|
|
124
124
|
}
|
|
125
125
|
function normalizeResearchMode(value) {
|
|
126
|
-
return value === "deep"
|
|
126
|
+
return value === "deep" || value === "search" ? value : "off";
|
|
127
127
|
}
|
|
128
128
|
function normalizeArchiveMode(value) {
|
|
129
129
|
return value === "always" || value === "never" ? value : "auto";
|
|
@@ -2,6 +2,8 @@ import { mkdtemp, rm, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import net from "node:net";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { claimBrowserTarget } from "./targetClaim.js";
|
|
5
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
8
|
import { copyChromeProfile } from "./profileCopy.js";
|
|
7
9
|
import { BrowserCancellation, withoutBrowserCancellation } from "./cancellation.js";
|
|
@@ -554,19 +556,13 @@ export function isLocalChromeHostForTest(host) {
|
|
|
554
556
|
return isLocalChromeHost(host);
|
|
555
557
|
}
|
|
556
558
|
async function closeRemoteConnectionAfterRun(options) {
|
|
557
|
-
if (options.connectionClosedUnexpectedly) {
|
|
558
|
-
return;
|
|
559
|
-
}
|
|
560
559
|
if (!options.connection) {
|
|
561
560
|
await options.client?.close();
|
|
562
561
|
return;
|
|
563
562
|
}
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
await options.client?.close();
|
|
569
|
-
}
|
|
563
|
+
await options.connection.close({
|
|
564
|
+
preserveTarget: options.connectionClosedUnexpectedly || options.preserveTarget,
|
|
565
|
+
});
|
|
570
566
|
}
|
|
571
567
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
572
568
|
return (options.ownsTarget &&
|
|
@@ -712,8 +708,11 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
712
708
|
let lastTargetId;
|
|
713
709
|
let lastUrl;
|
|
714
710
|
let promptSubmitted = false;
|
|
711
|
+
let ownedRecoveryTarget;
|
|
712
|
+
const targetClaimId = randomUUID();
|
|
715
713
|
let modelSelectionEvidence;
|
|
716
714
|
let thinkingSelectionEvidence;
|
|
715
|
+
let researchPlan;
|
|
717
716
|
let tabLease = null;
|
|
718
717
|
let conversationUrlMonitor = null;
|
|
719
718
|
const emitRuntimeHint = async () => {
|
|
@@ -729,8 +728,10 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
729
728
|
tabUrl: lastUrl,
|
|
730
729
|
conversationId,
|
|
731
730
|
promptSubmitted,
|
|
731
|
+
ownedRecoveryTarget,
|
|
732
732
|
userDataDir,
|
|
733
733
|
controllerPid: process.pid,
|
|
734
|
+
researchPlan,
|
|
734
735
|
};
|
|
735
736
|
try {
|
|
736
737
|
await runtimeHintCb?.(hint, modelSelectionEvidence);
|
|
@@ -929,7 +930,15 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
929
930
|
});
|
|
930
931
|
client = cancellation.client(connection.client);
|
|
931
932
|
isolatedTargetId = connection.targetId ?? null;
|
|
932
|
-
ownsTarget =
|
|
933
|
+
ownsTarget = Boolean(connection.targetId);
|
|
934
|
+
if (connection.targetId && (!config.keepBrowser || options.closeOwnedTabOnComplete)) {
|
|
935
|
+
ownedRecoveryTarget = {
|
|
936
|
+
host: chromeHost,
|
|
937
|
+
port: chrome.port,
|
|
938
|
+
targetId: connection.targetId,
|
|
939
|
+
claimId: targetClaimId,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
933
942
|
}
|
|
934
943
|
if (tabLease && isolatedTargetId) {
|
|
935
944
|
await tabLease.update({
|
|
@@ -977,7 +986,9 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
977
986
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
978
987
|
: undefined,
|
|
979
988
|
promptSubmitted,
|
|
989
|
+
ownedRecoveryTarget,
|
|
980
990
|
controllerPid: process.pid,
|
|
991
|
+
researchPlan,
|
|
981
992
|
},
|
|
982
993
|
}));
|
|
983
994
|
})();
|
|
@@ -990,6 +1001,8 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
990
1001
|
domainEnablers.push(DOM.enable());
|
|
991
1002
|
}
|
|
992
1003
|
await Promise.all(domainEnablers);
|
|
1004
|
+
if (config.browserTabRef)
|
|
1005
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
993
1006
|
if (!config.headless && config.hideWindow) {
|
|
994
1007
|
await positionChromeWindowOffscreen(client, userDataDir, logger);
|
|
995
1008
|
}
|
|
@@ -1251,6 +1264,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1251
1264
|
await withoutBrowserCancellation(() => handle.release()).catch(() => undefined);
|
|
1252
1265
|
};
|
|
1253
1266
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
1267
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
1254
1268
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
1255
1269
|
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
1256
1270
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
@@ -1313,6 +1327,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1313
1327
|
attachmentNames: attachmentExpectations,
|
|
1314
1328
|
attachmentNavigationUrl,
|
|
1315
1329
|
onPromptSubmitted: markPromptSubmitted,
|
|
1330
|
+
webSearch: config.researchMode === "search",
|
|
1316
1331
|
};
|
|
1317
1332
|
const deepResearchTargetBaseline = deepResearch && client
|
|
1318
1333
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
@@ -1387,7 +1402,17 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1387
1402
|
}
|
|
1388
1403
|
const imageArtifactMinTurnIndex = baselineTurns;
|
|
1389
1404
|
if (deepResearch) {
|
|
1390
|
-
await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger
|
|
1405
|
+
await raceWithDisconnect(waitForResearchPlanAutoConfirm(Runtime, logger, undefined, {
|
|
1406
|
+
Page,
|
|
1407
|
+
client,
|
|
1408
|
+
ignoredTargetKeys: deepResearchTargetKeys,
|
|
1409
|
+
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
1410
|
+
minTurnIndex: baselineTurns,
|
|
1411
|
+
onPlan: async (plan) => {
|
|
1412
|
+
researchPlan = plan;
|
|
1413
|
+
await emitRuntimeHint();
|
|
1414
|
+
},
|
|
1415
|
+
}));
|
|
1391
1416
|
const researchResult = await raceWithDisconnect(waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
|
|
1392
1417
|
ignoredTargetKeys: deepResearchTargetKeys,
|
|
1393
1418
|
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
@@ -1438,7 +1463,9 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1438
1463
|
tabUrl: lastUrl,
|
|
1439
1464
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1440
1465
|
promptSubmitted,
|
|
1466
|
+
ownedRecoveryTarget,
|
|
1441
1467
|
controllerPid: process.pid,
|
|
1468
|
+
researchPlan,
|
|
1442
1469
|
};
|
|
1443
1470
|
}
|
|
1444
1471
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
@@ -1528,6 +1555,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1528
1555
|
tabUrl: lastUrl,
|
|
1529
1556
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1530
1557
|
promptSubmitted,
|
|
1558
|
+
ownedRecoveryTarget,
|
|
1531
1559
|
controllerPid: process.pid,
|
|
1532
1560
|
},
|
|
1533
1561
|
});
|
|
@@ -1584,6 +1612,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1584
1612
|
tabUrl: lastUrl,
|
|
1585
1613
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1586
1614
|
promptSubmitted,
|
|
1615
|
+
ownedRecoveryTarget,
|
|
1587
1616
|
controllerPid: process.pid,
|
|
1588
1617
|
};
|
|
1589
1618
|
throw await createAssistantTimeoutError({
|
|
@@ -1809,6 +1838,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1809
1838
|
tabUrl: lastUrl,
|
|
1810
1839
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1811
1840
|
promptSubmitted,
|
|
1841
|
+
ownedRecoveryTarget,
|
|
1812
1842
|
controllerPid: process.pid,
|
|
1813
1843
|
},
|
|
1814
1844
|
}),
|
|
@@ -1876,6 +1906,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1876
1906
|
tabUrl: lastUrl,
|
|
1877
1907
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
1878
1908
|
promptSubmitted,
|
|
1909
|
+
ownedRecoveryTarget,
|
|
1879
1910
|
controllerPid: process.pid,
|
|
1880
1911
|
};
|
|
1881
1912
|
}
|
|
@@ -1902,6 +1933,7 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1902
1933
|
chromeTargetId: lastTargetId,
|
|
1903
1934
|
tabUrl: lastUrl,
|
|
1904
1935
|
promptSubmitted,
|
|
1936
|
+
ownedRecoveryTarget,
|
|
1905
1937
|
controllerPid: process.pid,
|
|
1906
1938
|
};
|
|
1907
1939
|
const reuseProfileHint = `oracle --engine browser --browser-manual-login ` +
|
|
@@ -1965,7 +1997,9 @@ async function runBrowserModeInternal(options, cancellation) {
|
|
|
1965
1997
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1966
1998
|
: undefined,
|
|
1967
1999
|
promptSubmitted,
|
|
2000
|
+
ownedRecoveryTarget,
|
|
1968
2001
|
controllerPid: process.pid,
|
|
2002
|
+
researchPlan,
|
|
1969
2003
|
},
|
|
1970
2004
|
}, normalizedError);
|
|
1971
2005
|
}
|
|
@@ -2363,8 +2397,11 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2363
2397
|
let tabLease = null;
|
|
2364
2398
|
let lastUrl;
|
|
2365
2399
|
let promptSubmitted = false;
|
|
2400
|
+
let ownedRecoveryTarget;
|
|
2401
|
+
const targetClaimId = randomUUID();
|
|
2366
2402
|
let modelSelectionEvidence;
|
|
2367
2403
|
let thinkingSelectionEvidence;
|
|
2404
|
+
let researchPlan;
|
|
2368
2405
|
let attachedExistingTab = false;
|
|
2369
2406
|
let ownsTarget = true;
|
|
2370
2407
|
let conversationUrlMonitor = null;
|
|
@@ -2382,7 +2419,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2382
2419
|
tabUrl: lastUrl,
|
|
2383
2420
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2384
2421
|
promptSubmitted,
|
|
2422
|
+
ownedRecoveryTarget,
|
|
2385
2423
|
controllerPid: process.pid,
|
|
2424
|
+
researchPlan,
|
|
2386
2425
|
}, modelSelectionEvidence);
|
|
2387
2426
|
await tabLease?.update({
|
|
2388
2427
|
chromeHost: host,
|
|
@@ -2452,7 +2491,16 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2452
2491
|
}), (connection) => connection.close());
|
|
2453
2492
|
client = cancellation.client(connection.client);
|
|
2454
2493
|
remoteTargetId = connection.targetId ?? null;
|
|
2455
|
-
ownsTarget =
|
|
2494
|
+
ownsTarget = Boolean(connection.targetId);
|
|
2495
|
+
if (connection.targetId && (!config.keepBrowser || options.closeOwnedTabOnComplete)) {
|
|
2496
|
+
ownedRecoveryTarget = {
|
|
2497
|
+
host,
|
|
2498
|
+
port,
|
|
2499
|
+
targetId: connection.targetId,
|
|
2500
|
+
browserWSEndpoint,
|
|
2501
|
+
claimId: targetClaimId,
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2456
2504
|
}
|
|
2457
2505
|
if (tabLease && remoteTargetId) {
|
|
2458
2506
|
await tabLease.update({
|
|
@@ -2472,6 +2520,8 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2472
2520
|
domainEnablers.push(DOM.enable());
|
|
2473
2521
|
}
|
|
2474
2522
|
await Promise.all(domainEnablers);
|
|
2523
|
+
if (config.browserTabRef)
|
|
2524
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
2475
2525
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
2476
2526
|
await enableFocusEmulation(client, logger, "remote target");
|
|
2477
2527
|
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
@@ -2572,6 +2622,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2572
2622
|
});
|
|
2573
2623
|
}
|
|
2574
2624
|
const submitOnce = async (prompt, submissionAttachments) => {
|
|
2625
|
+
await claimBrowserTarget(Runtime, targetClaimId);
|
|
2575
2626
|
const baselineSnapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
2576
2627
|
const baselineAssistantText = typeof baselineSnapshot?.text === "string" ? baselineSnapshot.text.trim() : "";
|
|
2577
2628
|
const attachmentNames = submissionAttachments.map((a) => path.basename(a.path));
|
|
@@ -2629,6 +2680,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2629
2680
|
attachmentNames: attachmentExpectations,
|
|
2630
2681
|
attachmentNavigationUrl,
|
|
2631
2682
|
onPromptSubmitted: markPromptSubmitted,
|
|
2683
|
+
webSearch: config.researchMode === "search",
|
|
2632
2684
|
};
|
|
2633
2685
|
const deepResearchTargetBaseline = deepResearch && client
|
|
2634
2686
|
? await captureDeepResearchTargetBaseline(client, logger)
|
|
@@ -2679,7 +2731,17 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2679
2731
|
deepResearchTargetBaselineCaptured = submission.deepResearchTargetBaselineCaptured ?? false;
|
|
2680
2732
|
const imageArtifactMinTurnIndex = baselineTurns;
|
|
2681
2733
|
if (deepResearch) {
|
|
2682
|
-
await waitForResearchPlanAutoConfirm(Runtime, logger
|
|
2734
|
+
await waitForResearchPlanAutoConfirm(Runtime, logger, undefined, {
|
|
2735
|
+
Page,
|
|
2736
|
+
client,
|
|
2737
|
+
ignoredTargetKeys: deepResearchTargetKeys,
|
|
2738
|
+
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
2739
|
+
minTurnIndex: baselineTurns,
|
|
2740
|
+
onPlan: async (plan) => {
|
|
2741
|
+
researchPlan = plan;
|
|
2742
|
+
await emitRuntimeHint();
|
|
2743
|
+
},
|
|
2744
|
+
});
|
|
2683
2745
|
const researchResult = await waitForDeepResearchCompletion(Runtime, logger, config.timeoutMs, baselineTurns, Page, client, {
|
|
2684
2746
|
ignoredTargetKeys: deepResearchTargetKeys,
|
|
2685
2747
|
targetBaselineCaptured: deepResearchTargetBaselineCaptured,
|
|
@@ -2728,7 +2790,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2728
2790
|
tabUrl: lastUrl,
|
|
2729
2791
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2730
2792
|
promptSubmitted,
|
|
2793
|
+
ownedRecoveryTarget,
|
|
2731
2794
|
controllerPid: process.pid,
|
|
2795
|
+
researchPlan,
|
|
2732
2796
|
};
|
|
2733
2797
|
}
|
|
2734
2798
|
// Helper to normalize text for echo detection (collapse whitespace, lowercase)
|
|
@@ -2817,6 +2881,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2817
2881
|
tabUrl: lastUrl,
|
|
2818
2882
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2819
2883
|
promptSubmitted,
|
|
2884
|
+
ownedRecoveryTarget,
|
|
2820
2885
|
controllerPid: process.pid,
|
|
2821
2886
|
},
|
|
2822
2887
|
});
|
|
@@ -2875,6 +2940,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2875
2940
|
tabUrl: lastUrl,
|
|
2876
2941
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
2877
2942
|
promptSubmitted,
|
|
2943
|
+
ownedRecoveryTarget,
|
|
2878
2944
|
controllerPid: process.pid,
|
|
2879
2945
|
};
|
|
2880
2946
|
throw await createAssistantTimeoutError({
|
|
@@ -3058,6 +3124,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3058
3124
|
tabUrl: lastUrl,
|
|
3059
3125
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
3060
3126
|
promptSubmitted,
|
|
3127
|
+
ownedRecoveryTarget,
|
|
3061
3128
|
controllerPid: process.pid,
|
|
3062
3129
|
},
|
|
3063
3130
|
}),
|
|
@@ -3120,6 +3187,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3120
3187
|
tabUrl: lastUrl,
|
|
3121
3188
|
conversationId: lastUrl ? extractConversationIdFromUrl(lastUrl) : undefined,
|
|
3122
3189
|
promptSubmitted,
|
|
3190
|
+
ownedRecoveryTarget,
|
|
3123
3191
|
artifacts: savedArtifacts,
|
|
3124
3192
|
generatedImages: imageArtifacts.generatedImages,
|
|
3125
3193
|
savedImages: imageArtifacts.savedImages,
|
|
@@ -3168,7 +3236,9 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3168
3236
|
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
3169
3237
|
: undefined,
|
|
3170
3238
|
promptSubmitted,
|
|
3239
|
+
ownedRecoveryTarget,
|
|
3171
3240
|
controllerPid: process.pid,
|
|
3241
|
+
researchPlan,
|
|
3172
3242
|
},
|
|
3173
3243
|
});
|
|
3174
3244
|
}
|
|
@@ -3176,17 +3246,6 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3176
3246
|
await withoutBrowserCancellation(async () => {
|
|
3177
3247
|
stopThinkingMonitor?.();
|
|
3178
3248
|
await conversationUrlMonitor?.stop();
|
|
3179
|
-
try {
|
|
3180
|
-
await closeRemoteConnectionAfterRun({
|
|
3181
|
-
connectionClosedUnexpectedly,
|
|
3182
|
-
connection,
|
|
3183
|
-
client,
|
|
3184
|
-
runStatus,
|
|
3185
|
-
});
|
|
3186
|
-
}
|
|
3187
|
-
catch {
|
|
3188
|
-
// ignore
|
|
3189
|
-
}
|
|
3190
3249
|
removeDialogHandler?.();
|
|
3191
3250
|
const keepRemoteBrowser = Boolean(config.keepBrowser);
|
|
3192
3251
|
const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
@@ -3196,33 +3255,42 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
3196
3255
|
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
3197
3256
|
closeOwnedTabOnCancel: options.closeOwnedTabOnCancel,
|
|
3198
3257
|
});
|
|
3199
|
-
const
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
if (!replacementTargetId) {
|
|
3213
|
-
logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
|
|
3258
|
+
const closeConnection = async () => {
|
|
3259
|
+
let preserveTarget = !shouldCloseOwnedRemoteTarget;
|
|
3260
|
+
if (!preserveTarget && keepRemoteBrowser && client && remoteTargetId) {
|
|
3261
|
+
try {
|
|
3262
|
+
const { targetInfos } = await client.Target.getTargets();
|
|
3263
|
+
if (!targetInfos.some((target) => target.type === "page" && target.targetId !== remoteTargetId)) {
|
|
3264
|
+
const replacement = await client.Target.createTarget({ url: "about:blank" });
|
|
3265
|
+
if (!replacement.targetId)
|
|
3266
|
+
preserveTarget = true;
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
catch {
|
|
3270
|
+
preserveTarget = true;
|
|
3214
3271
|
}
|
|
3215
3272
|
}
|
|
3273
|
+
await closeRemoteConnectionAfterRun({
|
|
3274
|
+
connectionClosedUnexpectedly,
|
|
3275
|
+
connection,
|
|
3276
|
+
client,
|
|
3277
|
+
preserveTarget,
|
|
3278
|
+
});
|
|
3216
3279
|
};
|
|
3217
3280
|
if (tabLease) {
|
|
3218
3281
|
const handle = tabLease;
|
|
3219
3282
|
tabLease = null;
|
|
3220
|
-
await handle
|
|
3221
|
-
|
|
3222
|
-
|
|
3283
|
+
await handle.release({ onRelease: closeConnection }).catch(async () => {
|
|
3284
|
+
await closeRemoteConnectionAfterRun({
|
|
3285
|
+
connectionClosedUnexpectedly,
|
|
3286
|
+
connection,
|
|
3287
|
+
client,
|
|
3288
|
+
preserveTarget: true,
|
|
3289
|
+
}).catch(() => undefined);
|
|
3290
|
+
});
|
|
3223
3291
|
}
|
|
3224
3292
|
else {
|
|
3225
|
-
await
|
|
3293
|
+
await closeConnection().catch(() => undefined);
|
|
3226
3294
|
}
|
|
3227
3295
|
// Don't kill remote Chrome - it's not ours to manage
|
|
3228
3296
|
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import CDP from "chrome-remote-interface";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, INPUT_SELECTORS, MODEL_BUTTON_SELECTOR, SEND_BUTTON_SELECTORS,
|
|
3
|
+
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, INPUT_SELECTORS, MODEL_BUTTON_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTORS, } from "./constants.js";
|
|
4
4
|
import { captureAssistantMarkdown, readAssistantSnapshot } from "./actions/assistantResponse.js";
|
|
5
5
|
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
6
6
|
import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
|
|
@@ -62,7 +62,7 @@ function buildTabInspectionExpression() {
|
|
|
62
62
|
const answerSelectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
63
63
|
const assistantRoleLiteral = escapeLiteral(ASSISTANT_ROLE_SELECTOR);
|
|
64
64
|
const modelButtonSelectorLiteral = escapeLiteral(MODEL_BUTTON_SELECTOR);
|
|
65
|
-
const stopSelectorLiteral = escapeLiteral(
|
|
65
|
+
const stopSelectorLiteral = escapeLiteral(STOP_BUTTON_SELECTORS.join(","));
|
|
66
66
|
return `(() => {
|
|
67
67
|
const INPUT_SELECTORS = ${inputSelectorsLiteral};
|
|
68
68
|
const SEND_SELECTORS = ${sendSelectorsLiteral};
|
|
@@ -91,8 +91,7 @@ function buildTabInspectionExpression() {
|
|
|
91
91
|
const label = normalize(node.textContent || node.getAttribute('aria-label') || node.getAttribute('title'));
|
|
92
92
|
return LOGIN_CTA.test(label);
|
|
93
93
|
});
|
|
94
|
-
const
|
|
95
|
-
const stopExists = Boolean(stopButton && isVisible(stopButton));
|
|
94
|
+
const stopExists = Array.from(document.querySelectorAll(STOP_BUTTON_SELECTOR)).some(isVisible);
|
|
96
95
|
const sendButton = firstVisible(SEND_SELECTORS);
|
|
97
96
|
const sendExists = Boolean(sendButton);
|
|
98
97
|
const promptNode = firstVisible(INPUT_SELECTORS);
|
|
@@ -27,6 +27,7 @@ async function submitPromptViaAdapter(ctx) {
|
|
|
27
27
|
inputTimeoutMs: state.inputTimeoutMs ?? undefined,
|
|
28
28
|
attachmentTimeoutMs: state.attachmentTimeoutMs ?? undefined,
|
|
29
29
|
onPromptSubmitted: state.onPromptSubmitted,
|
|
30
|
+
webSearch: state.webSearch,
|
|
30
31
|
}, ctx.prompt, state.logger);
|
|
31
32
|
state.committedTurns =
|
|
32
33
|
typeof committedTurns === "number" && Number.isFinite(committedTurns) ? committedTurns : null;
|
|
@@ -66,6 +66,19 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
66
66
|
closeAttachedConnection = () => connection.close();
|
|
67
67
|
const client = connection.client;
|
|
68
68
|
const { Runtime, DOM, Page } = client;
|
|
69
|
+
const captureIdentity = async () => {
|
|
70
|
+
const targetId = target?.targetId ?? target?.id;
|
|
71
|
+
if (!targetId || !port)
|
|
72
|
+
return undefined;
|
|
73
|
+
const { result } = await withTimeout(Runtime.evaluate({ expression: "location.href", returnByValue: true }), 2_000, "Recovery target identity unavailable");
|
|
74
|
+
return {
|
|
75
|
+
host,
|
|
76
|
+
port,
|
|
77
|
+
targetId,
|
|
78
|
+
browserWSEndpoint,
|
|
79
|
+
conversationId: extractConversationIdFromUrl(typeof result?.value === "string" ? result.value : ""),
|
|
80
|
+
};
|
|
81
|
+
};
|
|
69
82
|
if (Runtime?.enable) {
|
|
70
83
|
await Runtime.enable();
|
|
71
84
|
}
|
|
@@ -117,10 +130,12 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
117
130
|
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
118
131
|
requireScopedTargetOwner: true,
|
|
119
132
|
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
133
|
+
const captureTarget = await captureIdentity().catch(() => undefined);
|
|
120
134
|
await closeAttached();
|
|
121
135
|
return {
|
|
122
136
|
answerText: researchResult.text,
|
|
123
137
|
answerMarkdown: researchResult.text,
|
|
138
|
+
captureTarget,
|
|
124
139
|
};
|
|
125
140
|
}
|
|
126
141
|
const promptEcho = buildPromptEchoMatcher(deps.promptPreview);
|
|
@@ -128,8 +143,13 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
128
143
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
129
144
|
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
130
145
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
146
|
+
const captureTarget = await captureIdentity().catch(() => undefined);
|
|
131
147
|
await closeAttached();
|
|
132
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
answerText: aligned.answerText,
|
|
150
|
+
answerMarkdown: aligned.answerMarkdown,
|
|
151
|
+
captureTarget,
|
|
152
|
+
};
|
|
133
153
|
}
|
|
134
154
|
catch (error) {
|
|
135
155
|
await closeAttached();
|