@steipete/oracle 0.15.2 → 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/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/chromeLifecycle.js +29 -28
- 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/index.js +56 -16
- package/dist/src/browser/liveTabs.js +113 -31
- 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/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/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +9 -10
- 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, closeRemoteChromeTarget, 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";
|
|
@@ -281,6 +281,25 @@ async function createAssistantTimeoutError(params) {
|
|
|
281
281
|
}
|
|
282
282
|
return warningError;
|
|
283
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
|
+
}
|
|
284
303
|
function listIgnoredRemoteChromeFlags(config) {
|
|
285
304
|
return [
|
|
286
305
|
config.headless ? "--browser-headless" : null,
|
|
@@ -356,13 +375,13 @@ async function pollGeneratedImageOrTextAssistantResponse(Runtime, timeoutMs, min
|
|
|
356
375
|
}
|
|
357
376
|
return null;
|
|
358
377
|
}
|
|
359
|
-
function isImageOnlyUiChromeText(text) {
|
|
378
|
+
export function isImageOnlyUiChromeText(text) {
|
|
360
379
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
361
380
|
return (normalized.length === 0 ||
|
|
362
381
|
normalized === "edit" ||
|
|
363
382
|
normalized === "stopped thinking" ||
|
|
364
383
|
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));
|
|
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));
|
|
366
385
|
}
|
|
367
386
|
function normalizeBrowserFollowUpPrompts(values) {
|
|
368
387
|
return (values ?? []).map((entry) => entry.trim()).filter(Boolean);
|
|
@@ -783,14 +802,18 @@ export async function runBrowserMode(options) {
|
|
|
783
802
|
});
|
|
784
803
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
785
804
|
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
786
|
-
if (!config.headless && config.hideWindow) {
|
|
787
|
-
await hideChromeWindow(chrome, logger);
|
|
788
|
-
}
|
|
789
805
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
790
806
|
if (DOM && typeof DOM.enable === "function") {
|
|
791
807
|
domainEnablers.push(DOM.enable());
|
|
792
808
|
}
|
|
793
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");
|
|
794
817
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
795
818
|
if (!profileIsPreSigned) {
|
|
796
819
|
await Network.clearBrowserCookies();
|
|
@@ -911,6 +934,18 @@ export async function runBrowserMode(options) {
|
|
|
911
934
|
}));
|
|
912
935
|
}
|
|
913
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
|
+
}
|
|
914
949
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
915
950
|
const captureRuntimeSnapshot = async () => {
|
|
916
951
|
try {
|
|
@@ -1961,7 +1996,7 @@ function detachKeptChromeProcess(chrome) {
|
|
|
1961
1996
|
// Best-effort only; cleanup should not mask the original browser result.
|
|
1962
1997
|
}
|
|
1963
1998
|
}
|
|
1964
|
-
async function acquireManualLoginChromeForRun(userDataDir, config, logger, sessionId, deps = {}) {
|
|
1999
|
+
export async function acquireManualLoginChromeForRun(userDataDir, config, logger, sessionId, deps = {}) {
|
|
1965
2000
|
const maybeReuse = deps.maybeReuse ?? maybeReuseRunningChrome;
|
|
1966
2001
|
const launch = deps.launch ?? launchChrome;
|
|
1967
2002
|
const lockTimeoutMs = Math.max(0, config.profileLockTimeoutMs ?? 0);
|
|
@@ -2176,14 +2211,7 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2176
2211
|
}
|
|
2177
2212
|
await Promise.all(domainEnablers);
|
|
2178
2213
|
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
|
-
}
|
|
2214
|
+
await enableFocusEmulation(client, logger, "remote target");
|
|
2187
2215
|
const activeConversationUrlMonitor = createConversationUrlMonitor({
|
|
2188
2216
|
readUrl: async () => {
|
|
2189
2217
|
const { result } = await Runtime.evaluate({
|
|
@@ -2222,6 +2250,18 @@ async function runRemoteBrowserMode(promptText, attachments, config, logger, opt
|
|
|
2222
2250
|
expectedConversationUrl: config.resumeConversationUrl,
|
|
2223
2251
|
});
|
|
2224
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
|
+
}
|
|
2225
2265
|
logger(`Prompt textarea ready (initial focus, ${promptText.length.toLocaleString()} chars queued)`);
|
|
2226
2266
|
try {
|
|
2227
2267
|
const { result } = await Runtime.evaluate({
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import CDP from "chrome-remote-interface";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR,
|
|
3
|
+
import { ANSWER_SELECTORS, ASSISTANT_ROLE_SELECTOR, INPUT_SELECTORS, MODEL_BUTTON_SELECTOR, SEND_BUTTON_SELECTORS, STOP_BUTTON_SELECTOR, } from "./constants.js";
|
|
4
4
|
import { captureAssistantMarkdown, readAssistantSnapshot } from "./actions/assistantResponse.js";
|
|
5
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
5
6
|
import { delay } from "./utils.js";
|
|
6
7
|
export const DEFAULT_REMOTE_CHROME_HOST = "127.0.0.1";
|
|
7
8
|
export const DEFAULT_REMOTE_CHROME_PORT = 9222;
|
|
@@ -58,7 +59,6 @@ function buildTabInspectionExpression() {
|
|
|
58
59
|
const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
59
60
|
const sendSelectorsLiteral = JSON.stringify(SEND_BUTTON_SELECTORS);
|
|
60
61
|
const answerSelectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
61
|
-
const turnSelectorLiteral = escapeLiteral(CONVERSATION_TURN_SELECTOR);
|
|
62
62
|
const assistantRoleLiteral = escapeLiteral(ASSISTANT_ROLE_SELECTOR);
|
|
63
63
|
const modelButtonSelectorLiteral = escapeLiteral(MODEL_BUTTON_SELECTOR);
|
|
64
64
|
const stopSelectorLiteral = escapeLiteral(STOP_BUTTON_SELECTOR);
|
|
@@ -66,7 +66,6 @@ function buildTabInspectionExpression() {
|
|
|
66
66
|
const INPUT_SELECTORS = ${inputSelectorsLiteral};
|
|
67
67
|
const SEND_SELECTORS = ${sendSelectorsLiteral};
|
|
68
68
|
const ANSWER_SELECTORS = ${answerSelectorsLiteral};
|
|
69
|
-
const TURN_SELECTOR = ${turnSelectorLiteral};
|
|
70
69
|
const ASSISTANT_ROLE_SELECTOR = ${assistantRoleLiteral};
|
|
71
70
|
const MODEL_BUTTON_SELECTOR = ${modelButtonSelectorLiteral};
|
|
72
71
|
const STOP_BUTTON_SELECTOR = ${stopSelectorLiteral};
|
|
@@ -97,15 +96,21 @@ function buildTabInspectionExpression() {
|
|
|
97
96
|
const sendExists = Boolean(sendButton);
|
|
98
97
|
const promptNode = firstVisible(INPUT_SELECTORS);
|
|
99
98
|
const promptReady = Boolean(promptNode);
|
|
100
|
-
const turns =
|
|
99
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
101
100
|
const assistantTurns = turns.filter((turn) => {
|
|
102
101
|
const role = normalize(turn.getAttribute('data-message-author-role') || turn.getAttribute('data-turn')).toLowerCase();
|
|
103
102
|
if (role === 'assistant') return true;
|
|
104
103
|
return Boolean(turn.querySelector(ASSISTANT_ROLE_SELECTOR));
|
|
105
104
|
});
|
|
105
|
+
const fallbackUserTurns = Array.from(
|
|
106
|
+
document.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'),
|
|
107
|
+
);
|
|
106
108
|
const userTurns = turns.filter((turn) => {
|
|
107
109
|
const role = normalize(turn.getAttribute('data-message-author-role') || turn.getAttribute('data-turn')).toLowerCase();
|
|
108
|
-
|
|
110
|
+
if (role === 'user') return true;
|
|
111
|
+
return Boolean(
|
|
112
|
+
turn.querySelector('[data-message-author-role="user"], [data-turn="user"]'),
|
|
113
|
+
);
|
|
109
114
|
});
|
|
110
115
|
const answerNode = ANSWER_SELECTORS
|
|
111
116
|
.map((selector) => document.querySelectorAll(selector))
|
|
@@ -116,16 +121,47 @@ function buildTabInspectionExpression() {
|
|
|
116
121
|
if (currentModelLabel === 'ChatGPT' && hasProPill) {
|
|
117
122
|
currentModelLabel = 'ChatGPT + Pro';
|
|
118
123
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
124
|
+
const rawAnswerNodes = Array.from(answerNode || []);
|
|
125
|
+
const userCandidates = Array.from(new Set([...userTurns, ...fallbackUserTurns]));
|
|
126
|
+
const lastUserTurn = userCandidates.reduce((latest, candidate) => {
|
|
127
|
+
if (!latest) return candidate;
|
|
128
|
+
return latest.compareDocumentPosition(candidate) & 4 ? candidate : latest;
|
|
129
|
+
}, null);
|
|
130
|
+
const lastUserContainer = lastUserTurn
|
|
131
|
+
? turns.find((turn) => turn === lastUserTurn || turn.contains?.(lastUserTurn))
|
|
132
|
+
: null;
|
|
133
|
+
const answerNodes = rawAnswerNodes.filter(
|
|
134
|
+
(node) =>
|
|
135
|
+
!lastUserTurn ||
|
|
136
|
+
(node !== lastUserTurn &&
|
|
137
|
+
!lastUserTurn.contains?.(node) &&
|
|
138
|
+
!node.contains?.(lastUserTurn)),
|
|
139
|
+
);
|
|
140
|
+
const answerTexts = answerNodes.map((node) => normalize(node.textContent)).filter(Boolean);
|
|
141
|
+
const assistantCandidates = Array.from(new Set([...assistantTurns, ...answerNodes]));
|
|
142
|
+
const lastAssistantNode = assistantCandidates.reduce((latest, candidate) => {
|
|
143
|
+
if (!latest) return candidate;
|
|
144
|
+
return latest.compareDocumentPosition(candidate) & 4 ? candidate : latest;
|
|
145
|
+
}, null);
|
|
146
|
+
const lastAssistantContainer = lastAssistantNode
|
|
147
|
+
? turns.find((turn) => turn === lastAssistantNode || turn.contains?.(lastAssistantNode))
|
|
148
|
+
: null;
|
|
149
|
+
const assistantFollowsLatestUser = Boolean(
|
|
150
|
+
lastAssistantNode &&
|
|
151
|
+
lastUserTurn &&
|
|
152
|
+
lastAssistantNode !== lastUserTurn &&
|
|
153
|
+
(lastUserTurn.compareDocumentPosition(lastAssistantNode) & 4),
|
|
154
|
+
);
|
|
155
|
+
const lastAssistantTurnIndex = lastAssistantContainer
|
|
156
|
+
? turns.indexOf(lastAssistantContainer)
|
|
157
|
+
: -1;
|
|
158
|
+
const lastUserTurnIndex = lastUserContainer ? turns.indexOf(lastUserContainer) : -1;
|
|
159
|
+
const assistantOwners = assistantCandidates.map(
|
|
160
|
+
(node) => turns.find((turn) => turn === node || turn.contains?.(node)) || node,
|
|
161
|
+
);
|
|
162
|
+
const assistantCount = new Set(assistantOwners).size;
|
|
163
|
+
const lastAssistantText = normalize(lastAssistantNode?.textContent);
|
|
164
|
+
const lastUserText = normalize(lastUserTurn?.textContent);
|
|
129
165
|
const authenticated = !loginButtonExists && (promptReady || sendExists || stopExists || assistantCount > 0);
|
|
130
166
|
return {
|
|
131
167
|
title: normalize(document.title),
|
|
@@ -138,12 +174,18 @@ function buildTabInspectionExpression() {
|
|
|
138
174
|
authenticated,
|
|
139
175
|
assistantCount,
|
|
140
176
|
lastAssistantText,
|
|
177
|
+
assistantFollowsLatestUser,
|
|
178
|
+
lastAssistantTurnIndex,
|
|
179
|
+
lastUserTurnIndex,
|
|
141
180
|
lastUserText,
|
|
142
181
|
visibilityState: document.visibilityState,
|
|
143
182
|
focused: Boolean(document.hasFocus?.()),
|
|
144
183
|
};
|
|
145
184
|
})()`;
|
|
146
185
|
}
|
|
186
|
+
export function buildTabInspectionExpressionForTest() {
|
|
187
|
+
return buildTabInspectionExpression();
|
|
188
|
+
}
|
|
147
189
|
export async function listChatGptTargets(options = {}) {
|
|
148
190
|
const { host, port } = normalizeHostPort(options);
|
|
149
191
|
const targets = (await CDP.List({ host, port }));
|
|
@@ -183,7 +225,20 @@ export async function inspectChatGptTab(options) {
|
|
|
183
225
|
});
|
|
184
226
|
const info = (evaluation.result?.value ?? {});
|
|
185
227
|
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
186
|
-
const
|
|
228
|
+
const inspectedAssistantTurnIndex = typeof info.lastAssistantTurnIndex === "number" && info.lastAssistantTurnIndex >= 0
|
|
229
|
+
? info.lastAssistantTurnIndex
|
|
230
|
+
: undefined;
|
|
231
|
+
const normalizedSnapshotText = normalizeTitle(snapshot?.text ?? "").toLowerCase();
|
|
232
|
+
const normalizedInspectedText = normalizeTitle(info.lastAssistantText ?? "").toLowerCase();
|
|
233
|
+
const snapshotMatchesInspectedTurn = (typeof snapshot?.turnIndex === "number" &&
|
|
234
|
+
snapshot.turnIndex === inspectedAssistantTurnIndex) ||
|
|
235
|
+
(snapshot?.turnIndex == null &&
|
|
236
|
+
inspectedAssistantTurnIndex === undefined &&
|
|
237
|
+
normalizedSnapshotText.length > 0 &&
|
|
238
|
+
normalizedSnapshotText === normalizedInspectedText);
|
|
239
|
+
const lastAssistantText = snapshotMatchesInspectedTurn &&
|
|
240
|
+
typeof snapshot?.text === "string" &&
|
|
241
|
+
snapshot.text.trim().length > 0
|
|
187
242
|
? snapshot.text.trim()
|
|
188
243
|
: String(info.lastAssistantText ?? "").trim();
|
|
189
244
|
const lastUserText = String(info.lastUserText ?? "").trim();
|
|
@@ -201,6 +256,11 @@ export async function inspectChatGptTab(options) {
|
|
|
201
256
|
authenticated: Boolean(info.authenticated),
|
|
202
257
|
assistantCount: Number.isFinite(info.assistantCount) ? Number(info.assistantCount) : 0,
|
|
203
258
|
lastAssistantText,
|
|
259
|
+
assistantFollowsLatestUser: Boolean(info.assistantFollowsLatestUser),
|
|
260
|
+
lastAssistantTurnIndex: inspectedAssistantTurnIndex,
|
|
261
|
+
lastUserTurnIndex: typeof info.lastUserTurnIndex === "number" && info.lastUserTurnIndex >= 0
|
|
262
|
+
? info.lastUserTurnIndex
|
|
263
|
+
: undefined,
|
|
204
264
|
lastAssistantSnippet: trimToSnippet(lastAssistantText),
|
|
205
265
|
lastUserText,
|
|
206
266
|
lastUserSnippet: trimToSnippet(lastUserText),
|
|
@@ -210,8 +270,12 @@ export async function inspectChatGptTab(options) {
|
|
|
210
270
|
fingerprint: "",
|
|
211
271
|
state: "detached",
|
|
212
272
|
lastAssistantMarkdown: null,
|
|
213
|
-
lastAssistantMessageId: typeof snapshot?.messageId === "string"
|
|
214
|
-
|
|
273
|
+
lastAssistantMessageId: snapshotMatchesInspectedTurn && typeof snapshot?.messageId === "string"
|
|
274
|
+
? snapshot.messageId
|
|
275
|
+
: undefined,
|
|
276
|
+
lastAssistantTurnId: snapshotMatchesInspectedTurn && typeof snapshot?.turnId === "string"
|
|
277
|
+
? snapshot.turnId
|
|
278
|
+
: undefined,
|
|
215
279
|
};
|
|
216
280
|
summary.state = classifyTabState(summary);
|
|
217
281
|
summary.fingerprint = buildTargetFingerprint(summary);
|
|
@@ -292,6 +356,10 @@ function resolveChatGptTabFromSummaries(summaries, ref) {
|
|
|
292
356
|
if (exactUrl) {
|
|
293
357
|
return exactUrl;
|
|
294
358
|
}
|
|
359
|
+
const exactConversationId = summaries.find((tab) => tab.conversationId === trimmedRef);
|
|
360
|
+
if (exactConversationId) {
|
|
361
|
+
return exactConversationId;
|
|
362
|
+
}
|
|
295
363
|
const lower = trimmedRef.toLowerCase();
|
|
296
364
|
const titleMatches = summaries.filter((tab) => tab.title.toLowerCase().includes(lower));
|
|
297
365
|
if (titleMatches.length === 1) {
|
|
@@ -328,17 +396,6 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
328
396
|
try {
|
|
329
397
|
const { Runtime } = client;
|
|
330
398
|
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
331
|
-
let assistantMarkdown = null;
|
|
332
|
-
if (snapshot?.messageId || snapshot?.turnId) {
|
|
333
|
-
assistantMarkdown = await captureAssistantMarkdown(Runtime, {
|
|
334
|
-
messageId: snapshot.messageId,
|
|
335
|
-
turnId: snapshot.turnId,
|
|
336
|
-
}, noopLogger).catch(() => null);
|
|
337
|
-
}
|
|
338
|
-
const latestText = typeof snapshot?.text === "string" && snapshot.text.trim().length > 0
|
|
339
|
-
? snapshot.text.trim()
|
|
340
|
-
: resolved.lastAssistantText;
|
|
341
|
-
const lastAssistantText = latestText ?? "";
|
|
342
399
|
const nowSummary = await inspectChatGptTab({
|
|
343
400
|
host,
|
|
344
401
|
port,
|
|
@@ -349,15 +406,37 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
349
406
|
type: "page",
|
|
350
407
|
},
|
|
351
408
|
});
|
|
409
|
+
const normalizedSnapshotText = normalizeTitle(snapshot?.text ?? "").toLowerCase();
|
|
410
|
+
const normalizedInspectedText = normalizeTitle(nowSummary.lastAssistantText).toLowerCase();
|
|
411
|
+
const snapshotMatchesLatestTurn = (typeof snapshot?.turnIndex === "number" &&
|
|
412
|
+
snapshot.turnIndex === nowSummary.lastAssistantTurnIndex) ||
|
|
413
|
+
(snapshot?.turnIndex == null &&
|
|
414
|
+
nowSummary.lastAssistantTurnIndex === undefined &&
|
|
415
|
+
normalizedSnapshotText.length > 0 &&
|
|
416
|
+
normalizedSnapshotText === normalizedInspectedText);
|
|
417
|
+
let assistantMarkdown = null;
|
|
418
|
+
if (snapshotMatchesLatestTurn && (snapshot?.messageId || snapshot?.turnId)) {
|
|
419
|
+
assistantMarkdown = await captureAssistantMarkdown(Runtime, {
|
|
420
|
+
messageId: snapshot.messageId,
|
|
421
|
+
turnId: snapshot.turnId,
|
|
422
|
+
}, noopLogger).catch(() => null);
|
|
423
|
+
}
|
|
424
|
+
const lastAssistantText = snapshotMatchesLatestTurn &&
|
|
425
|
+
typeof snapshot?.text === "string" &&
|
|
426
|
+
snapshot.text.trim().length > 0
|
|
427
|
+
? snapshot.text.trim()
|
|
428
|
+
: nowSummary.lastAssistantText;
|
|
352
429
|
const harvested = {
|
|
353
430
|
...nowSummary,
|
|
354
431
|
lastAssistantText,
|
|
355
432
|
lastAssistantSnippet: trimToSnippet(lastAssistantText),
|
|
356
433
|
lastAssistantMarkdown: assistantMarkdown ?? (lastAssistantText || null),
|
|
357
|
-
lastAssistantMessageId: typeof snapshot?.messageId === "string"
|
|
434
|
+
lastAssistantMessageId: snapshotMatchesLatestTurn && typeof snapshot?.messageId === "string"
|
|
358
435
|
? snapshot.messageId
|
|
359
436
|
: nowSummary.lastAssistantMessageId,
|
|
360
|
-
lastAssistantTurnId: typeof snapshot?.turnId === "string"
|
|
437
|
+
lastAssistantTurnId: snapshotMatchesLatestTurn && typeof snapshot?.turnId === "string"
|
|
438
|
+
? snapshot.turnId
|
|
439
|
+
: nowSummary.lastAssistantTurnId,
|
|
361
440
|
};
|
|
362
441
|
if (harvested.stopExists && options.stallWindowMs && options.stallWindowMs > 0) {
|
|
363
442
|
const firstFingerprint = harvested.fingerprint;
|
|
@@ -383,6 +462,9 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
383
462
|
harvested.loginButtonExists = followup.loginButtonExists;
|
|
384
463
|
harvested.lastUserText = followup.lastUserText;
|
|
385
464
|
harvested.lastUserSnippet = followup.lastUserSnippet;
|
|
465
|
+
harvested.assistantFollowsLatestUser = followup.assistantFollowsLatestUser;
|
|
466
|
+
harvested.lastAssistantTurnIndex = followup.lastAssistantTurnIndex;
|
|
467
|
+
harvested.lastUserTurnIndex = followup.lastUserTurnIndex;
|
|
386
468
|
harvested.fingerprint = followup.fingerprint;
|
|
387
469
|
harvested.state =
|
|
388
470
|
harvested.stopExists && firstFingerprint === followup.fingerprint
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
|
|
1
|
+
export { navigateToChatGPT, navigateToPromptReadyWithFallback, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, ensureChatMode, waitForResumedConversationHydration, installJavaScriptDialogAutoDismissal, } from "./actions/navigation.js";
|
|
2
2
|
export { ensureModelSelection } from "./actions/modelSelection.js";
|
|
3
3
|
export { submitPrompt, clearPromptComposer } from "./actions/promptComposer.js";
|
|
4
4
|
export { clearComposerAttachments, uploadAttachmentFile, waitForAttachmentCompletion, waitForUserTurnAttachments, buildUserTurnAttachmentExpressionForTest, } from "./actions/attachments.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { closeTab, connectWithNewTab,
|
|
4
|
+
import { closeTab, connectWithNewTab, launchChrome, positionChromeWindowOffscreen, registerTerminationHooks, } from "./chromeLifecycle.js";
|
|
5
5
|
import { resolveBrowserConfig } from "./config.js";
|
|
6
6
|
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
7
7
|
import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
|
|
@@ -121,14 +121,14 @@ export async function runBrowserProjectSources(request) {
|
|
|
121
121
|
});
|
|
122
122
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
123
123
|
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
124
|
-
if (!config.headless && config.hideWindow) {
|
|
125
|
-
await hideChromeWindow(chrome, logger);
|
|
126
|
-
}
|
|
127
124
|
const domainEnablers = [Network.enable({}), Page.enable(), Runtime.enable()];
|
|
128
125
|
if (DOM && typeof DOM.enable === "function") {
|
|
129
126
|
domainEnablers.push(DOM.enable());
|
|
130
127
|
}
|
|
131
128
|
await Promise.all(domainEnablers);
|
|
129
|
+
if (!config.headless && config.hideWindow) {
|
|
130
|
+
await positionChromeWindowOffscreen(client, logger);
|
|
131
|
+
}
|
|
132
132
|
removeDialogHandler = installJavaScriptDialogAutoDismissal(Page, logger);
|
|
133
133
|
if (!manualLogin) {
|
|
134
134
|
await Network.clearBrowserCookies();
|
|
@@ -3,7 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
|
5
5
|
import { waitForAssistantResponse, captureAssistantMarkdown, navigateToChatGPT, ensureNotBlocked, ensureLoggedIn, ensurePromptReady, } from "./pageActions.js";
|
|
6
|
-
import { launchChrome, connectToChrome,
|
|
6
|
+
import { launchChrome, connectToChrome, positionChromeWindowOffscreen, connectToRemoteChromeTarget, listRemoteChromeTargets, } from "./chromeLifecycle.js";
|
|
7
7
|
import { resolveBrowserConfig } from "./config.js";
|
|
8
8
|
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
9
|
import { CHATGPT_URL } from "./constants.js";
|
|
@@ -180,7 +180,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
180
180
|
await DOM.enable();
|
|
181
181
|
}
|
|
182
182
|
if (!resolved.headless && resolved.hideWindow) {
|
|
183
|
-
await
|
|
183
|
+
await positionChromeWindowOffscreen(client, logger);
|
|
184
184
|
}
|
|
185
185
|
let appliedCookies = 0;
|
|
186
186
|
if (!manualLogin && resolved.cookieSync) {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isAnswerNowPlaceholderText } from "./actions/assistantResponse.js";
|
|
2
|
+
import { resolveBrowserConfig } from "./config.js";
|
|
3
|
+
import { acquireManualLoginChromeForRun, isImageOnlyUiChromeText } from "./index.js";
|
|
3
4
|
import { isRecoverableChatGptConversationUrl } from "./reattachability.js";
|
|
4
|
-
|
|
5
|
+
import { harvestChatGptTab, openChatGptTarget } from "./liveTabs.js";
|
|
6
|
+
const DEFAULT_READY_TIMEOUT_MS = 30_000;
|
|
7
|
+
const READY_POLL_MS = 1_000;
|
|
5
8
|
/**
|
|
6
9
|
* Picks the URL to navigate the recovered Chrome tab to.
|
|
7
10
|
*
|
|
@@ -23,12 +26,54 @@ export function resolveRecoveryUrl(meta) {
|
|
|
23
26
|
}
|
|
24
27
|
return null;
|
|
25
28
|
}
|
|
26
|
-
function
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
export function resolveRecoveryProfileDir(meta) {
|
|
30
|
+
const config = meta?.browser?.config;
|
|
31
|
+
const resolved = resolveBrowserConfig(config);
|
|
32
|
+
if (!resolved.manualLogin) {
|
|
33
|
+
throw new Error("Cannot recover conversation: session was not run with a manual-login browser profile.");
|
|
30
34
|
}
|
|
31
|
-
|
|
35
|
+
const runtime = meta?.browser?.runtime;
|
|
36
|
+
const profileDir = runtime?.userDataDir ?? resolved.manualLoginProfileDir;
|
|
37
|
+
if (typeof profileDir !== "string" || profileDir.trim().length === 0) {
|
|
38
|
+
throw new Error("Cannot recover conversation: session metadata has no recorded manual-login profile directory.");
|
|
39
|
+
}
|
|
40
|
+
return profileDir;
|
|
41
|
+
}
|
|
42
|
+
async function waitForRecoveredConversationReady(endpoint, ref, timeoutMs) {
|
|
43
|
+
const deadline = Date.now() + timeoutMs;
|
|
44
|
+
let lastError = null;
|
|
45
|
+
while (Date.now() < deadline) {
|
|
46
|
+
try {
|
|
47
|
+
const harvested = await harvestChatGptTab({ ...endpoint, ref });
|
|
48
|
+
if (isRecoveredConversationHarvestReady(harvested)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
lastError = new Error(`recovered tab is still ${harvested.state}`);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
lastError = error;
|
|
55
|
+
}
|
|
56
|
+
await new Promise((resolve) => setTimeout(resolve, READY_POLL_MS));
|
|
57
|
+
}
|
|
58
|
+
const suffix = lastError instanceof Error ? ` Last error: ${lastError.message}` : "";
|
|
59
|
+
throw new Error(`Recovered ChatGPT conversation did not become ready in time.${suffix}`);
|
|
60
|
+
}
|
|
61
|
+
export function isRecoveredConversationHarvestReady(harvested) {
|
|
62
|
+
const latestAssistant = harvested.lastAssistantText ??
|
|
63
|
+
harvested.lastAssistantMarkdown ??
|
|
64
|
+
harvested.lastAssistantSnippet ??
|
|
65
|
+
"";
|
|
66
|
+
const assistantFollowsLatestUser = harvested.assistantFollowsLatestUser === true ||
|
|
67
|
+
(typeof harvested.lastAssistantTurnIndex === "number" &&
|
|
68
|
+
typeof harvested.lastUserTurnIndex === "number" &&
|
|
69
|
+
harvested.lastAssistantTurnIndex > harvested.lastUserTurnIndex);
|
|
70
|
+
return (harvested.stopExists === true ||
|
|
71
|
+
((harvested.assistantCount ?? 0) > 0 &&
|
|
72
|
+
assistantFollowsLatestUser &&
|
|
73
|
+
latestAssistant.trim().length > 0 &&
|
|
74
|
+
!isImageOnlyUiChromeText(latestAssistant) &&
|
|
75
|
+
!isAnswerNowPlaceholderText(latestAssistant) &&
|
|
76
|
+
!/^answer now$/i.test(latestAssistant.trim())));
|
|
32
77
|
}
|
|
33
78
|
/**
|
|
34
79
|
* Re-open a previously-harvested ChatGPT conversation by relaunching Chrome
|
|
@@ -46,28 +91,44 @@ export async function recoverConversationTab(meta, logger, options = {}) {
|
|
|
46
91
|
throw new Error("Cannot recover conversation: session metadata has no recoverable ChatGPT conversation URL " +
|
|
47
92
|
"(expected browser.harvest.url or browser.runtime.tabUrl to be a chatgpt.com/c/<id> URL).");
|
|
48
93
|
}
|
|
49
|
-
const
|
|
94
|
+
const readyTimeoutMs = options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
|
|
95
|
+
const waitForReady = options.waitForReady !== false;
|
|
96
|
+
if (options.existingEndpoint) {
|
|
97
|
+
try {
|
|
98
|
+
logger(`[browser] Recovery: opening saved conversation in existing Chrome at ` +
|
|
99
|
+
`${options.existingEndpoint.host}:${options.existingEndpoint.port}`);
|
|
100
|
+
const targetId = await openChatGptTarget({ ...options.existingEndpoint, url });
|
|
101
|
+
if (waitForReady) {
|
|
102
|
+
await waitForRecoveredConversationReady(options.existingEndpoint, targetId, readyTimeoutMs);
|
|
103
|
+
}
|
|
104
|
+
return { ...options.existingEndpoint, url, ref: targetId, chrome: null };
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
108
|
+
logger(`[browser] Recovery: existing Chrome could not reopen the conversation (${message}).`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const userDataDir = resolveRecoveryProfileDir(meta);
|
|
112
|
+
const config = resolveBrowserConfig(meta.browser?.config);
|
|
50
113
|
logger(`[browser] Recovery: relaunching Chrome with profile ${userDataDir} and navigating to ${url}`);
|
|
51
|
-
const chrome = await
|
|
52
|
-
|
|
53
|
-
"--no-first-run",
|
|
54
|
-
"--no-default-browser-check",
|
|
55
|
-
"--disable-features=AutomationControlled,TranslateUI",
|
|
56
|
-
"--disable-sync",
|
|
57
|
-
"--password-store=basic",
|
|
58
|
-
"--use-mock-keychain",
|
|
59
|
-
"--lang=en-US",
|
|
60
|
-
url,
|
|
61
|
-
],
|
|
62
|
-
userDataDir,
|
|
63
|
-
handleSIGINT: false,
|
|
64
|
-
});
|
|
65
|
-
const host = "127.0.0.1";
|
|
114
|
+
const { chrome } = await acquireManualLoginChromeForRun(userDataDir, config, logger, meta.id, {});
|
|
115
|
+
const host = chrome.host ?? "127.0.0.1";
|
|
66
116
|
const port = chrome.port;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
117
|
+
try {
|
|
118
|
+
const targetId = await openChatGptTarget({ host, port, url });
|
|
119
|
+
if (waitForReady) {
|
|
120
|
+
await waitForRecoveredConversationReady({ host, port }, targetId, readyTimeoutMs);
|
|
121
|
+
}
|
|
122
|
+
logger(`[browser] Recovery: Chrome listening on ${host}:${port}; tab loaded.`);
|
|
123
|
+
return { host, port, url, ref: targetId, chrome };
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
chrome.kill();
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// best-effort cleanup
|
|
131
|
+
}
|
|
132
|
+
throw error;
|
|
70
133
|
}
|
|
71
|
-
logger(`[browser] Recovery: Chrome listening on ${host}:${port}; tab loaded.`);
|
|
72
|
-
return { host, port, url, chrome };
|
|
73
134
|
}
|
|
@@ -16,6 +16,8 @@ const DEFAULT_CHROME_PROFILE = "Default";
|
|
|
16
16
|
// The browser label is passed to the model picker which fuzzy-matches against ChatGPT's UI.
|
|
17
17
|
const BROWSER_MODEL_LABELS = [
|
|
18
18
|
// Most specific first (e.g., "gpt-5.2-thinking" before "gpt-5.2")
|
|
19
|
+
["gpt-5.6-sol", "GPT-5.6 Sol"],
|
|
20
|
+
["gpt-5.6", "GPT-5.6 Sol"],
|
|
19
21
|
["gpt-5.5-pro", "Pro"],
|
|
20
22
|
["gpt-5.5-instant", "GPT-5.5 Instant"],
|
|
21
23
|
["gpt-5.5", "Thinking 5.5"],
|
|
@@ -40,7 +42,9 @@ export function normalizeChatGptModelForBrowser(model) {
|
|
|
40
42
|
if (!normalized.startsWith("gpt-") || normalized.includes("codex")) {
|
|
41
43
|
return model;
|
|
42
44
|
}
|
|
43
|
-
if (normalized === "gpt-5.
|
|
45
|
+
if (normalized === "gpt-5.6-sol" ||
|
|
46
|
+
normalized === "gpt-5.6" ||
|
|
47
|
+
normalized === "gpt-5.5-pro" ||
|
|
44
48
|
normalized === "gpt-5.5-instant" ||
|
|
45
49
|
normalized === "gpt-5.5" ||
|
|
46
50
|
normalized === "gpt-5.4") {
|