@steipete/oracle 0.15.2 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/bin/oracle-cli.js +1 -1
- package/dist/bin/oracle-mcp.js +0 -0
- package/dist/src/browser/actions/assistantResponse.js +218 -126
- package/dist/src/browser/actions/modelSelection.js +133 -22
- package/dist/src/browser/actions/navigation.js +235 -14
- package/dist/src/browser/actions/thinkingStatus.js +228 -0
- package/dist/src/browser/actions/thinkingTime.js +68 -11
- package/dist/src/browser/cdpLiveness.js +78 -0
- package/dist/src/browser/chromeLifecycle.js +112 -30
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/constants.js +5 -1
- package/dist/src/browser/controlPlan.js +2 -2
- package/dist/src/browser/conversationUrl.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +2 -4
- package/dist/src/browser/cookies.js +6 -4
- package/dist/src/browser/index.js +213 -57
- package/dist/src/browser/liveTabs.js +115 -33
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/projectSourcesRunner.js +4 -4
- package/dist/src/browser/reattach.js +2 -2
- package/dist/src/browser/reattachHelpers.js +3 -5
- package/dist/src/browser/reattachability.js +2 -1
- package/dist/src/browser/recoverConversation.js +90 -29
- package/dist/src/browser/tabLeaseRegistry.js +9 -3
- package/dist/src/cli/browserConfig.js +5 -1
- package/dist/src/cli/browserDefaults.js +4 -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/cli/sessionRunner.js +49 -2
- package/dist/src/oracle/config.js +25 -0
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/remote/server.js +8 -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 +13 -14
- 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
|
@@ -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,
|
|
7
|
+
import { launchChrome, registerTerminationHooks, positionChromeWindowOffscreen, connectToRemoteChrome, connectWithNewTab, closeTab, createChromePageTarget, ensureChromePageTargetAfterClose, closeBlankChromeTabs, } from "./chromeLifecycle.js";
|
|
8
8
|
import { clearStaleChatGptConversationCookies, 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";
|
|
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";
|
|
@@ -19,6 +19,7 @@ import { BrowserAutomationError } from "../oracle/errors.js";
|
|
|
19
19
|
import { alignPromptEchoPair, buildPromptEchoMatcher } from "./reattachHelpers.js";
|
|
20
20
|
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
21
21
|
import { cleanupStaleProfileState, acquireProfileRunLock, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, terminateRecordedChromeForProfile, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
22
|
+
import { connectionLostUserMessage, isRecoverableChromeDisconnect, probeChromeTargetLiveness, } from "./cdpLiveness.js";
|
|
22
23
|
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
23
24
|
import { appendArtifacts, saveBrowserTranscriptArtifact, saveDeepResearchReportArtifact, } from "./artifacts.js";
|
|
24
25
|
import { collectGeneratedImageArtifacts } from "./chatgptImages.js";
|
|
@@ -32,6 +33,7 @@ import { archiveChatGptConversation, resolveBrowserArchiveDecision, } from "./ac
|
|
|
32
33
|
import { assertManualLoginProfileReadyForRun, defaultManualLoginProfileDir, formatManualLoginSetupCommand, isManualLoginProfileInitialized, resolveManualLoginWaitMs, } from "./manualLoginProfile.js";
|
|
33
34
|
import { describeBrowserControlPlan, formatBrowserControlPlan } from "./controlPlan.js";
|
|
34
35
|
import { createConversationUrlMonitor, } from "./conversationUrlMonitor.js";
|
|
36
|
+
import { extractStableConversationIdFromUrl as extractConversationIdFromUrl, isStableConversationUrl as isConversationUrl, } from "./conversationUrl.js";
|
|
35
37
|
export { CHATGPT_URL, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET } from "./constants.js";
|
|
36
38
|
export { parseDuration, delay, normalizeChatgptUrl, isTemporaryChatUrl } from "./utils.js";
|
|
37
39
|
export { formatThinkingLog, formatThinkingWaitingLog, buildThinkingStatusExpressionForTest, readThinkingStatusForTest, sanitizeThinkingText, startThinkingStatusMonitorForTest, } from "./actions/thinkingStatus.js";
|
|
@@ -281,6 +283,25 @@ async function createAssistantTimeoutError(params) {
|
|
|
281
283
|
}
|
|
282
284
|
return warningError;
|
|
283
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* Make the page behave like a focused foreground tab.
|
|
288
|
+
*
|
|
289
|
+
* The send button is activated with trusted CDP input events dispatched at
|
|
290
|
+
* viewport coordinates. Chrome delivers those only to a window that is being
|
|
291
|
+
* composited, so a hidden (`--browser-hide-window`), minimized, or occluded
|
|
292
|
+
* window swallows the click while the automation still believes it clicked.
|
|
293
|
+
* Soft-fails: focus emulation is an optimization, never a hard requirement.
|
|
294
|
+
*/
|
|
295
|
+
async function enableFocusEmulation(client, logger, label) {
|
|
296
|
+
try {
|
|
297
|
+
await client.Emulation.setFocusEmulationEnabled({ enabled: true });
|
|
298
|
+
logger(`[browser] Focus emulation enabled for ${label}`);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
302
|
+
logger(`[browser] Focus emulation unavailable: ${message}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
284
305
|
function listIgnoredRemoteChromeFlags(config) {
|
|
285
306
|
return [
|
|
286
307
|
config.headless ? "--browser-headless" : null,
|
|
@@ -356,13 +377,13 @@ async function pollGeneratedImageOrTextAssistantResponse(Runtime, timeoutMs, min
|
|
|
356
377
|
}
|
|
357
378
|
return null;
|
|
358
379
|
}
|
|
359
|
-
function isImageOnlyUiChromeText(text) {
|
|
380
|
+
export function isImageOnlyUiChromeText(text) {
|
|
360
381
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
361
382
|
return (normalized.length === 0 ||
|
|
362
383
|
normalized === "edit" ||
|
|
363
384
|
normalized === "stopped thinking" ||
|
|
364
385
|
normalized === "stopped thinking edit" ||
|
|
365
|
-
/^thought for \d+(?:\.\d+)?\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)\s+edit$/.test(normalized));
|
|
386
|
+
/^(?: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));
|
|
366
387
|
}
|
|
367
388
|
function normalizeBrowserFollowUpPrompts(values) {
|
|
368
389
|
return (values ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -516,7 +537,17 @@ async function closeRemoteConnectionAfterRun(options) {
|
|
|
516
537
|
}
|
|
517
538
|
}
|
|
518
539
|
function shouldCloseOwnedRunTargetAfterRun(options) {
|
|
519
|
-
return options.runStatus === "complete" &&
|
|
540
|
+
return (options.runStatus === "complete" &&
|
|
541
|
+
options.ownsTarget &&
|
|
542
|
+
(Boolean(options.closeOwnedTabOnComplete) || !options.keepBrowser));
|
|
543
|
+
}
|
|
544
|
+
function shouldCleanupBlankTabsAfterLastLease(options) {
|
|
545
|
+
return (options.runStatus === "complete" &&
|
|
546
|
+
options.ownsTarget &&
|
|
547
|
+
!options.connectionClosedUnexpectedly &&
|
|
548
|
+
options.manualLogin &&
|
|
549
|
+
options.keepBrowser &&
|
|
550
|
+
Boolean(options.chromePort));
|
|
520
551
|
}
|
|
521
552
|
function buildSkippedModelSelectionEvidence(desiredModel, strategy) {
|
|
522
553
|
return {
|
|
@@ -777,20 +808,54 @@ export async function runBrowserMode(options) {
|
|
|
777
808
|
const disconnectPromise = new Promise((_, reject) => {
|
|
778
809
|
client?.on("disconnect", () => {
|
|
779
810
|
connectionClosedUnexpectedly = true;
|
|
780
|
-
|
|
781
|
-
|
|
811
|
+
void (async () => {
|
|
812
|
+
const liveness = await probeChromeTargetLiveness({
|
|
813
|
+
host: chromeHost,
|
|
814
|
+
port: chrome.port,
|
|
815
|
+
targetId: lastTargetId ?? isolatedTargetId,
|
|
816
|
+
});
|
|
817
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
818
|
+
if (recoverable) {
|
|
819
|
+
logger("CDP client disconnected; Chrome/target still reachable. Leaving run recoverable for reattach.");
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
logger("Chrome window closed; attempting to abort run.");
|
|
823
|
+
}
|
|
824
|
+
reject(new BrowserAutomationError(connectionLostUserMessage({ recoverable }), {
|
|
825
|
+
stage: "connection-lost",
|
|
826
|
+
recoverableDisconnect: recoverable,
|
|
827
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
828
|
+
runtime: {
|
|
829
|
+
chromePid: chrome.pid,
|
|
830
|
+
chromePort: chrome.port,
|
|
831
|
+
chromeHost,
|
|
832
|
+
userDataDir,
|
|
833
|
+
chromeTargetId: lastTargetId ?? isolatedTargetId ?? undefined,
|
|
834
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
835
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
836
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
837
|
+
: undefined,
|
|
838
|
+
promptSubmitted,
|
|
839
|
+
controllerPid: process.pid,
|
|
840
|
+
},
|
|
841
|
+
}));
|
|
842
|
+
})();
|
|
782
843
|
});
|
|
783
844
|
});
|
|
784
845
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
785
846
|
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
786
|
-
if (!config.headless && config.hideWindow) {
|
|
787
|
-
await hideChromeWindow(chrome, logger);
|
|
788
|
-
}
|
|
789
847
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
790
848
|
if (DOM && typeof DOM.enable === "function") {
|
|
791
849
|
domainEnablers.push(DOM.enable());
|
|
792
850
|
}
|
|
793
851
|
await Promise.all(domainEnablers);
|
|
852
|
+
if (!config.headless && config.hideWindow) {
|
|
853
|
+
await positionChromeWindowOffscreen(client, logger);
|
|
854
|
+
}
|
|
855
|
+
// The send button is clicked with trusted CDP input events at viewport
|
|
856
|
+
// coordinates, which ChatGPT silently drops when the window is hidden or
|
|
857
|
+
// occluded. Emulate focus so the page behaves like a foreground tab.
|
|
858
|
+
await enableFocusEmulation(client, logger, "local target");
|
|
794
859
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
795
860
|
if (!profileIsPreSigned) {
|
|
796
861
|
await Network.clearBrowserCookies();
|
|
@@ -911,6 +976,18 @@ export async function runBrowserMode(options) {
|
|
|
911
976
|
}));
|
|
912
977
|
}
|
|
913
978
|
}
|
|
979
|
+
const chatMode = await raceWithDisconnect(ensureChatMode(Runtime, Input, config.inputTimeoutMs, logger, {
|
|
980
|
+
resetWorkConversation: config.browserTabRef && !isResumingConversation
|
|
981
|
+
? async () => {
|
|
982
|
+
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
983
|
+
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
984
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
985
|
+
}
|
|
986
|
+
: undefined,
|
|
987
|
+
}));
|
|
988
|
+
if (chatMode === "switched") {
|
|
989
|
+
await raceWithDisconnect(ensurePromptReady(Runtime, config.inputTimeoutMs, logger));
|
|
990
|
+
}
|
|
914
991
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
915
992
|
const captureRuntimeSnapshot = async () => {
|
|
916
993
|
try {
|
|
@@ -1692,19 +1769,34 @@ export async function runBrowserMode(options) {
|
|
|
1692
1769
|
throw normalizedError;
|
|
1693
1770
|
}
|
|
1694
1771
|
if ((config.debug || process.env.CHATGPT_DEVTOOLS_TRACE === "1") && normalizedError.stack) {
|
|
1695
|
-
logger(`Chrome
|
|
1772
|
+
logger(`Chrome connection lost before completion: ${normalizedError.message}`);
|
|
1696
1773
|
logger(normalizedError.stack);
|
|
1697
1774
|
}
|
|
1698
1775
|
await emitRuntimeHint();
|
|
1699
|
-
|
|
1776
|
+
if (normalizedError instanceof BrowserAutomationError &&
|
|
1777
|
+
normalizedError.details?.stage === "connection-lost") {
|
|
1778
|
+
throw normalizedError;
|
|
1779
|
+
}
|
|
1780
|
+
const liveness = await probeChromeTargetLiveness({
|
|
1781
|
+
host: chromeHost,
|
|
1782
|
+
port: chrome.port,
|
|
1783
|
+
targetId: lastTargetId ?? isolatedTargetId,
|
|
1784
|
+
});
|
|
1785
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
1786
|
+
throw new BrowserAutomationError(connectionLostUserMessage({ recoverable }), {
|
|
1700
1787
|
stage: "connection-lost",
|
|
1788
|
+
recoverableDisconnect: recoverable,
|
|
1789
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
1701
1790
|
runtime: {
|
|
1702
1791
|
chromePid: chrome.pid,
|
|
1703
1792
|
chromePort: chrome.port,
|
|
1704
1793
|
chromeHost,
|
|
1705
1794
|
userDataDir,
|
|
1706
1795
|
chromeTargetId: lastTargetId,
|
|
1707
|
-
tabUrl: lastUrl,
|
|
1796
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
1797
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
1798
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
1799
|
+
: undefined,
|
|
1708
1800
|
promptSubmitted,
|
|
1709
1801
|
controllerPid: process.pid,
|
|
1710
1802
|
},
|
|
@@ -1723,15 +1815,12 @@ export async function runBrowserMode(options) {
|
|
|
1723
1815
|
// Close the isolated tab once the response has been fully captured to prevent
|
|
1724
1816
|
// tab accumulation across repeated runs. Keep the tab open on incomplete runs
|
|
1725
1817
|
// so reattach can recover the response.
|
|
1726
|
-
|
|
1818
|
+
const shouldCloseOwnedRunTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
1727
1819
|
runStatus,
|
|
1728
1820
|
ownsTarget,
|
|
1729
1821
|
keepBrowser: effectiveKeepBrowser,
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
chrome?.port) {
|
|
1733
|
-
await closeTab(chrome.port, isolatedTargetId, logger, chromeHost).catch(() => undefined);
|
|
1734
|
-
}
|
|
1822
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
1823
|
+
});
|
|
1735
1824
|
let keepBrowserOpen = shouldKeepLocalBrowserOpen({
|
|
1736
1825
|
effectiveKeepBrowser,
|
|
1737
1826
|
preserveBrowserOnError,
|
|
@@ -1749,18 +1838,6 @@ export async function runBrowserMode(options) {
|
|
|
1749
1838
|
}
|
|
1750
1839
|
return otherActiveBrowserTabLeases;
|
|
1751
1840
|
};
|
|
1752
|
-
if (runStatus === "complete" &&
|
|
1753
|
-
manualLogin &&
|
|
1754
|
-
!connectionClosedUnexpectedly &&
|
|
1755
|
-
chrome?.port &&
|
|
1756
|
-
ownsTarget) {
|
|
1757
|
-
const otherLeasesActive = await hasOtherActiveLeases().catch(() => true);
|
|
1758
|
-
if (!otherLeasesActive) {
|
|
1759
|
-
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1760
|
-
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1761
|
-
}).catch(() => undefined);
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
1841
|
if (!keepBrowserOpen && manualLogin && tabLease) {
|
|
1765
1842
|
const cleanupLockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
1766
1843
|
if (cleanupLockTimeoutMs > 0) {
|
|
@@ -1778,10 +1855,55 @@ export async function runBrowserMode(options) {
|
|
|
1778
1855
|
terminatedRecordedChrome = await terminateRecordedChromeForProfile(userDataDir, logger).catch(() => false);
|
|
1779
1856
|
}
|
|
1780
1857
|
}
|
|
1858
|
+
const closeOwnedRunTarget = async () => {
|
|
1859
|
+
if (!shouldCloseOwnedRunTarget || !isolatedTargetId || !chrome?.port) {
|
|
1860
|
+
return;
|
|
1861
|
+
}
|
|
1862
|
+
const safeToClose = !effectiveKeepBrowser ||
|
|
1863
|
+
Boolean(await ensureChromePageTargetAfterClose(chrome.port, isolatedTargetId, logger, chromeHost));
|
|
1864
|
+
if (!safeToClose) {
|
|
1865
|
+
logger(`[browser] Leaving completed browser tab open because Chrome has no replacement page target.`);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
const closeConfirmed = await closeTab(chrome.port, isolatedTargetId, logger, chromeHost);
|
|
1869
|
+
if (!closeConfirmed && effectiveKeepBrowser) {
|
|
1870
|
+
const replacementTargetId = await createChromePageTarget(chrome.port, logger, chromeHost);
|
|
1871
|
+
if (!replacementTargetId) {
|
|
1872
|
+
logger(`[browser] Chrome page retention could not be verified after closing ${isolatedTargetId}.`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
const cleanupBlankTabs = async () => {
|
|
1877
|
+
if (!shouldCleanupBlankTabsAfterLastLease({
|
|
1878
|
+
runStatus,
|
|
1879
|
+
ownsTarget,
|
|
1880
|
+
connectionClosedUnexpectedly,
|
|
1881
|
+
manualLogin,
|
|
1882
|
+
keepBrowser: effectiveKeepBrowser,
|
|
1883
|
+
chromePort: chrome?.port,
|
|
1884
|
+
}) ||
|
|
1885
|
+
!chrome?.port) {
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
await closeBlankChromeTabs(chrome.port, logger, chromeHost, {
|
|
1889
|
+
excludeTargetIds: [isolatedTargetId, lastTargetId],
|
|
1890
|
+
preserveOneBlank: true,
|
|
1891
|
+
});
|
|
1892
|
+
};
|
|
1781
1893
|
if (tabLease) {
|
|
1782
1894
|
const handle = tabLease;
|
|
1783
1895
|
tabLease = null;
|
|
1784
|
-
|
|
1896
|
+
const onRelease = async ({ isLastLease }) => {
|
|
1897
|
+
await closeOwnedRunTarget();
|
|
1898
|
+
if (isLastLease) {
|
|
1899
|
+
await cleanupBlankTabs();
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
await handle.release({ onRelease }).catch(() => undefined);
|
|
1903
|
+
}
|
|
1904
|
+
else {
|
|
1905
|
+
await closeOwnedRunTarget();
|
|
1906
|
+
await cleanupBlankTabs();
|
|
1785
1907
|
}
|
|
1786
1908
|
removeDialogHandler?.();
|
|
1787
1909
|
removeTerminationHooks?.();
|
|
@@ -1961,7 +2083,7 @@ function detachKeptChromeProcess(chrome) {
|
|
|
1961
2083
|
// Best-effort only; cleanup should not mask the original browser result.
|
|
1962
2084
|
}
|
|
1963
2085
|
}
|
|
1964
|
-
async function acquireManualLoginChromeForRun(userDataDir, config, logger, sessionId, deps = {}) {
|
|
2086
|
+
export async function acquireManualLoginChromeForRun(userDataDir, config, logger, sessionId, deps = {}) {
|
|
1965
2087
|
const maybeReuse = deps.maybeReuse ?? maybeReuseRunningChrome;
|
|
1966
2088
|
const launch = deps.launch ?? launchChrome;
|
|
1967
2089
|
const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
@@ -2176,14 +2298,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2176
2298
|
}
|
|
2177
2299
|
await Promise.all(domainEnablers);
|
|
2178
2300
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
2179
|
-
|
|
2180
|
-
await client.Emulation.setFocusEmulationEnabled({ enabled: true });
|
|
2181
|
-
logger("[browser] Focus emulation enabled for remote target");
|
|
2182
|
-
}
|
|
2183
|
-
catch (error) {
|
|
2184
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2185
|
-
logger(`[browser] Focus emulation unavailable: ${message}`);
|
|
2186
|
-
}
|
|
2301
|
+
await enableFocusEmulation(client, logger, "remote target");
|
|
2187
2302
|
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
2188
2303
|
readUrl: async () => {
|
|
2189
2304
|
const { result } = await Runtime.evaluate({
|
|
@@ -2222,6 +2337,18 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2222
2337
|
expectedConversationUrl: config.resumeConversationUrl,
|
|
2223
2338
|
});
|
|
2224
2339
|
}
|
|
2340
|
+
const chatMode = await ensureChatMode(Runtime, Input, config.inputTimeoutMs, logger, {
|
|
2341
|
+
resetWorkConversation: attachedExistingTab && !config.resumeConversationUrl
|
|
2342
|
+
? async () => {
|
|
2343
|
+
await navigateToChatGPT(Page, Runtime, config.url, logger);
|
|
2344
|
+
await ensureNotBlocked(Runtime, config.headless, logger);
|
|
2345
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2346
|
+
}
|
|
2347
|
+
: undefined,
|
|
2348
|
+
});
|
|
2349
|
+
if (chatMode === "switched") {
|
|
2350
|
+
await ensurePromptReady(Runtime, config.inputTimeoutMs, logger);
|
|
2351
|
+
}
|
|
2225
2352
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
2226
2353
|
try {
|
|
2227
2354
|
const { result } = await Runtime.evaluate({
|
|
@@ -2829,15 +2956,27 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2829
2956
|
}
|
|
2830
2957
|
throw normalizedError;
|
|
2831
2958
|
}
|
|
2832
|
-
|
|
2959
|
+
const liveness = await probeChromeTargetLiveness({
|
|
2960
|
+
host,
|
|
2961
|
+
port,
|
|
2962
|
+
targetId: remoteTargetId,
|
|
2963
|
+
browserWSEndpoint,
|
|
2964
|
+
});
|
|
2965
|
+
const recoverable = isRecoverableChromeDisconnect(liveness);
|
|
2966
|
+
throw new BrowserAutomationError(connectionLostUserMessage({ recoverable, remote: true }), {
|
|
2833
2967
|
stage: "connection-lost",
|
|
2968
|
+
recoverableDisconnect: recoverable,
|
|
2969
|
+
disconnectCause: recoverable ? "cdp-client-disconnect" : "chrome-closed",
|
|
2834
2970
|
runtime: {
|
|
2835
2971
|
chromeHost: host,
|
|
2836
2972
|
chromePort: port,
|
|
2837
2973
|
chromeBrowserWSEndpoint: browserWSEndpoint,
|
|
2838
2974
|
chromeProfileRoot,
|
|
2839
2975
|
chromeTargetId: remoteTargetId ?? undefined,
|
|
2840
|
-
tabUrl: lastUrl,
|
|
2976
|
+
tabUrl: liveness.matchedUrl ?? lastUrl,
|
|
2977
|
+
conversationId: (liveness.matchedUrl ?? lastUrl)
|
|
2978
|
+
? extractConversationIdFromUrl(liveness.matchedUrl ?? lastUrl ?? "")
|
|
2979
|
+
: undefined,
|
|
2841
2980
|
promptSubmitted,
|
|
2842
2981
|
controllerPid: process.pid,
|
|
2843
2982
|
},
|
|
@@ -2857,17 +2996,40 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2857
2996
|
// ignore
|
|
2858
2997
|
}
|
|
2859
2998
|
removeDialogHandler?.();
|
|
2999
|
+
const keepRemoteBrowser = Boolean(config.keepBrowser);
|
|
3000
|
+
const shouldCloseOwnedRemoteTarget = shouldCloseOwnedRunTargetAfterRun({
|
|
3001
|
+
runStatus,
|
|
3002
|
+
ownsTarget,
|
|
3003
|
+
keepBrowser: keepRemoteBrowser,
|
|
3004
|
+
closeOwnedTabOnComplete: options.closeOwnedTabOnComplete,
|
|
3005
|
+
});
|
|
3006
|
+
const closeOwnedRemoteTarget = async () => {
|
|
3007
|
+
if (!shouldCloseOwnedRemoteTarget || !remoteTargetId) {
|
|
3008
|
+
return;
|
|
3009
|
+
}
|
|
3010
|
+
const safeToClose = !keepRemoteBrowser ||
|
|
3011
|
+
Boolean(await ensureChromePageTargetAfterClose(port, remoteTargetId, logger, host));
|
|
3012
|
+
if (!safeToClose) {
|
|
3013
|
+
logger(`[browser] Leaving completed remote browser tab open because Chrome has no replacement page target.`);
|
|
3014
|
+
return;
|
|
3015
|
+
}
|
|
3016
|
+
const closeConfirmed = await closeTab(port, remoteTargetId, logger, host);
|
|
3017
|
+
if (!closeConfirmed && keepRemoteBrowser) {
|
|
3018
|
+
const replacementTargetId = await createChromePageTarget(port, logger, host);
|
|
3019
|
+
if (!replacementTargetId) {
|
|
3020
|
+
logger(`[browser] Remote Chrome page retention could not be verified after closing ${remoteTargetId}.`);
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
};
|
|
2860
3024
|
if (tabLease) {
|
|
2861
3025
|
const handle = tabLease;
|
|
2862
3026
|
tabLease = null;
|
|
2863
|
-
await handle
|
|
3027
|
+
await handle
|
|
3028
|
+
.release({ onRelease: async () => closeOwnedRemoteTarget() })
|
|
3029
|
+
.catch(() => undefined);
|
|
2864
3030
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
ownsTarget,
|
|
2868
|
-
keepBrowser: Boolean(config.keepBrowser),
|
|
2869
|
-
})) {
|
|
2870
|
-
await closeRemoteChromeTarget(host, port, remoteTargetId ?? undefined, logger);
|
|
3031
|
+
else {
|
|
3032
|
+
await closeOwnedRemoteTarget();
|
|
2871
3033
|
}
|
|
2872
3034
|
// Don't kill remote Chrome - it's not ours to manage
|
|
2873
3035
|
const totalSeconds = (Date.now() - startedAt) / 1000;
|
|
@@ -2890,6 +3052,7 @@ export const __test__ = {
|
|
|
2890
3052
|
isImageOnlyUiChromeText,
|
|
2891
3053
|
listIgnoredRemoteChromeFlags,
|
|
2892
3054
|
resolveManualLoginWaitMs,
|
|
3055
|
+
shouldCleanupBlankTabsAfterLastLease,
|
|
2893
3056
|
shouldCloseOwnedRunTargetAfterRun,
|
|
2894
3057
|
shouldKeepLocalBrowserOpen,
|
|
2895
3058
|
};
|
|
@@ -3089,9 +3252,6 @@ async function readConversationTurnCount(Runtime, logger) {
|
|
|
3089
3252
|
}
|
|
3090
3253
|
return null;
|
|
3091
3254
|
}
|
|
3092
|
-
function isConversationUrl(url) {
|
|
3093
|
-
return /\/c\/[a-z0-9-]+/i.test(url);
|
|
3094
|
-
}
|
|
3095
3255
|
function describeDevtoolsFirewallHint(host, port) {
|
|
3096
3256
|
if (!isWsl())
|
|
3097
3257
|
return null;
|
|
@@ -3112,10 +3272,6 @@ function isWsl() {
|
|
|
3112
3272
|
return true;
|
|
3113
3273
|
return os.release().toLowerCase().includes("microsoft");
|
|
3114
3274
|
}
|
|
3115
|
-
function extractConversationIdFromUrl(url) {
|
|
3116
|
-
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
3117
|
-
return match?.[1];
|
|
3118
|
-
}
|
|
3119
3275
|
async function resolveUserDataBaseDir() {
|
|
3120
3276
|
// On WSL, Chrome launched via Windows can choke on UNC paths; prefer a Windows-backed temp folder.
|
|
3121
3277
|
if (isWsl()) {
|