@steipete/oracle 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/oracle-cli.js +1 -1
- package/dist/scripts/test-browser.js +13 -2
- package/dist/src/browser/actions/assistantResponse.js +235 -121
- package/dist/src/browser/actions/attachments.js +3 -4
- package/dist/src/browser/actions/deepResearch.js +6 -6
- package/dist/src/browser/actions/modelSelection.js +133 -22
- package/dist/src/browser/actions/navigation.js +235 -14
- package/dist/src/browser/actions/promptComposer.js +4 -5
- package/dist/src/browser/actions/thinkingStatus.js +228 -0
- package/dist/src/browser/actions/thinkingTime.js +68 -11
- package/dist/src/browser/chatgptFiles.js +4 -7
- package/dist/src/browser/chatgptImages.js +3 -4
- package/dist/src/browser/chromeLifecycle.js +30 -28
- package/dist/src/browser/config.js +14 -1
- package/dist/src/browser/constants.js +6 -1
- package/dist/src/browser/controlPlan.js +2 -2
- package/dist/src/browser/conversationTurns.js +16 -0
- package/dist/src/browser/conversationUrlMonitor.js +64 -0
- package/dist/src/browser/cookies.js +72 -0
- package/dist/src/browser/index.js +121 -72
- package/dist/src/browser/liveTabs.js +113 -31
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/projectSourcesRunner.js +7 -6
- package/dist/src/browser/reattach.js +29 -13
- package/dist/src/browser/reattachHelpers.js +14 -5
- package/dist/src/browser/recoverConversation.js +90 -29
- package/dist/src/cli/browserConfig.js +5 -1
- package/dist/src/cli/browserTabs.js +54 -33
- package/dist/src/cli/options.js +24 -0
- package/dist/src/cli/runOptions.js +6 -1
- package/dist/src/cli/sessionDisplay.js +38 -14
- package/dist/src/oracle/config.js +25 -0
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/oscProgress.js +3 -2
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +21 -22
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/dist/bin/oracle.js +0 -569
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +0 -1
- package/dist/docs-site/RELEASING.html +0 -410
- package/dist/docs-site/agents.html +0 -374
- package/dist/docs-site/anthropic.html +0 -368
- package/dist/docs-site/bridge.html +0 -416
- package/dist/docs-site/browser-mode.html +0 -594
- package/dist/docs-site/chromium-forks.html +0 -347
- package/dist/docs-site/cli-reference.html +0 -346
- package/dist/docs-site/configuration.html +0 -462
- package/dist/docs-site/favicon.svg +0 -14
- package/dist/docs-site/followup.html +0 -375
- package/dist/docs-site/gemini.html +0 -383
- package/dist/docs-site/grok.html +0 -325
- package/dist/docs-site/index.html +0 -360
- package/dist/docs-site/install.html +0 -335
- package/dist/docs-site/linux.html +0 -321
- package/dist/docs-site/llms.txt +0 -43
- package/dist/docs-site/manual-tests.html +0 -596
- package/dist/docs-site/mcp.html +0 -391
- package/dist/docs-site/multimodel.html +0 -364
- package/dist/docs-site/mythical-pro-agents.html +0 -360
- package/dist/docs-site/notifier.html +0 -338
- package/dist/docs-site/openai-endpoints.html +0 -399
- package/dist/docs-site/openrouter.html +0 -344
- package/dist/docs-site/quickstart.html +0 -369
- package/dist/docs-site/refactor/ux.html +0 -532
- package/dist/docs-site/sessions.html +0 -388
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +0 -79
- package/dist/docs-site/spec.html +0 -363
- package/dist/docs-site/testing.html +0 -320
- package/dist/docs-site/tui-debug.html +0 -326
- package/dist/docs-site/windows-work.html +0 -323
- package/dist/docs-site/windows.html +0 -320
- package/dist/src/browser/chromeCookies.js +0 -312
- package/dist/src/browser/keytarShim.js +0 -56
- package/dist/src/browser/windowsCookies.js +0 -219
|
@@ -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,9 +1,9 @@
|
|
|
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
|
-
import { syncCookies } from "./cookies.js";
|
|
6
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
7
7
|
import { installJavaScriptDialogAutoDismissal, navigateToChatGPT, ensureLoggedIn, } from "./pageActions.js";
|
|
8
8
|
import { acquireBrowserTabLease, hasOtherActiveBrowserTabLeases, } from "./tabLeaseRegistry.js";
|
|
9
9
|
import { acquireProfileRunLock, cleanupStaleProfileState, findRunningChromeDebugTargetForProfile, readChromePid, readDevToolsPort, shouldCleanupManualLoginProfileState, verifyDevToolsReachable, writeChromePid, writeDevToolsActivePort, } from "./profileState.js";
|
|
@@ -120,15 +120,15 @@ export async function runBrowserProjectSources(request) {
|
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
const raceWithDisconnect = (promise) => Promise.race([promise, disconnectPromise]);
|
|
123
|
-
const { Network, Page, Runtime, Input, DOM } = client;
|
|
124
|
-
if (!config.headless && config.hideWindow) {
|
|
125
|
-
await hideChromeWindow(chrome, logger);
|
|
126
|
-
}
|
|
123
|
+
const { Network, Page, Runtime, Input, DOM, Target } = client;
|
|
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();
|
|
@@ -139,6 +139,7 @@ export async function runBrowserProjectSources(request) {
|
|
|
139
139
|
manualLogin,
|
|
140
140
|
logger,
|
|
141
141
|
});
|
|
142
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger);
|
|
142
143
|
await raceWithDisconnect(navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger));
|
|
143
144
|
await raceWithDisconnect(waitForProjectSourcesLogin({
|
|
144
145
|
runtime: Runtime,
|
|
@@ -3,10 +3,11 @@ 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
|
-
import { syncCookies } from "./cookies.js";
|
|
9
|
-
import { CHATGPT_URL
|
|
8
|
+
import { clearStaleChatGptConversationCookies, syncCookies } from "./cookies.js";
|
|
9
|
+
import { CHATGPT_URL } from "./constants.js";
|
|
10
|
+
import { buildConversationTurnListExpression } from "./conversationTurns.js";
|
|
10
11
|
import { cleanupStaleProfileState } from "./profileState.js";
|
|
11
12
|
import { readDevToolsActivePortInfo } from "./detect.js";
|
|
12
13
|
import { pickTarget, extractConversationIdFromUrl, buildConversationUrl, withTimeout, openConversationFromSidebar, openConversationFromSidebarWithRetry, waitForLocationChange, readConversationTurnIndex, buildPromptEchoMatcher, recoverPromptEcho, alignPromptEchoMarkdown, } from "./reattachHelpers.js";
|
|
@@ -14,6 +15,12 @@ import { waitForDeepResearchCompletion } from "./actions/deepResearch.js";
|
|
|
14
15
|
export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
15
16
|
const recoverSession = deps.recoverSession ??
|
|
16
17
|
(async (runtimeMeta, configMeta) => resumeBrowserSessionViaNewChrome(runtimeMeta, configMeta, logger, deps));
|
|
18
|
+
let closeAttachedConnection = null;
|
|
19
|
+
const closeAttached = async () => {
|
|
20
|
+
const close = closeAttachedConnection;
|
|
21
|
+
closeAttachedConnection = null;
|
|
22
|
+
await close?.().catch(() => undefined);
|
|
23
|
+
};
|
|
17
24
|
if (!runtime.chromePort && !runtime.chromeBrowserWSEndpoint) {
|
|
18
25
|
logger("No running Chrome detected; reopening browser to locate the session.");
|
|
19
26
|
return recoverSession(runtime, config);
|
|
@@ -37,8 +44,8 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
37
44
|
targetId: target?.targetId ?? target?.id,
|
|
38
45
|
closeTargetOnDispose: false,
|
|
39
46
|
})
|
|
40
|
-
: {
|
|
41
|
-
client
|
|
47
|
+
: await (async () => {
|
|
48
|
+
const client = (await (deps.connect ?? ((options) => CDP(options)))(browserWSEndpoint
|
|
42
49
|
? {
|
|
43
50
|
target: browserWSEndpoint,
|
|
44
51
|
local: true,
|
|
@@ -48,9 +55,10 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
48
55
|
host,
|
|
49
56
|
port,
|
|
50
57
|
target: target?.targetId ?? target?.id,
|
|
51
|
-
}))
|
|
52
|
-
close:
|
|
53
|
-
};
|
|
58
|
+
}));
|
|
59
|
+
return { client, close: () => client.close() };
|
|
60
|
+
})();
|
|
61
|
+
closeAttachedConnection = () => connection.close();
|
|
54
62
|
const client = connection.client;
|
|
55
63
|
const { Runtime, DOM, Page } = client;
|
|
56
64
|
if (Runtime?.enable) {
|
|
@@ -97,7 +105,7 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
97
105
|
const researchResult = await withTimeout(waitForDeepResearch(Runtime, logger, timeoutMs, minTurnIndex ?? undefined, Page, client, {
|
|
98
106
|
requireScopedTargetOwner: true,
|
|
99
107
|
}), timeoutMs + 5_000, "Reattach Deep Research response timed out");
|
|
100
|
-
await
|
|
108
|
+
await closeAttached();
|
|
101
109
|
return {
|
|
102
110
|
answerText: researchResult.text,
|
|
103
111
|
answerMarkdown: researchResult.text,
|
|
@@ -108,10 +116,11 @@ export async function resumeBrowserSession(runtime, config, logger, deps = {}) {
|
|
|
108
116
|
const recovered = await recoverPromptEcho(Runtime, answer, promptEcho, logger, minTurnIndex, timeoutMs);
|
|
109
117
|
const markdown = (await withTimeout(captureMarkdown(Runtime, recovered.meta, logger), 15_000, "Reattach markdown capture timed out")) ?? recovered.text;
|
|
110
118
|
const aligned = alignPromptEchoMarkdown(recovered.text, markdown, promptEcho, logger);
|
|
111
|
-
await
|
|
119
|
+
await closeAttached();
|
|
112
120
|
return { answerText: aligned.answerText, answerMarkdown: aligned.answerMarkdown };
|
|
113
121
|
}
|
|
114
122
|
catch (error) {
|
|
123
|
+
await closeAttached();
|
|
115
124
|
const message = error instanceof Error ? error.message : String(error);
|
|
116
125
|
logger(`Existing Chrome reattach failed (${message}); reopening browser to locate the session.`);
|
|
117
126
|
return recoverSession(runtime, config);
|
|
@@ -163,7 +172,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
163
172
|
const chrome = await launchChrome(resolved, userDataDir, logger);
|
|
164
173
|
const chromeHost = chrome.host ?? "127.0.0.1";
|
|
165
174
|
const client = await connectToChrome(chrome.port, logger, chromeHost);
|
|
166
|
-
const { Network, Page, Runtime, DOM } = client;
|
|
175
|
+
const { Network, Page, Runtime, DOM, Target } = client;
|
|
167
176
|
if (Runtime?.enable) {
|
|
168
177
|
await Runtime.enable();
|
|
169
178
|
}
|
|
@@ -171,7 +180,7 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
171
180
|
await DOM.enable();
|
|
172
181
|
}
|
|
173
182
|
if (!resolved.headless && resolved.hideWindow) {
|
|
174
|
-
await
|
|
183
|
+
await positionChromeWindowOffscreen(client, logger);
|
|
175
184
|
}
|
|
176
185
|
let appliedCookies = 0;
|
|
177
186
|
if (!manualLogin && resolved.cookieSync) {
|
|
@@ -183,6 +192,13 @@ async function resumeBrowserSessionViaNewChrome(runtime, config, logger, deps) {
|
|
|
183
192
|
waitMs: resolved.cookieSyncWaitMs ?? 0,
|
|
184
193
|
});
|
|
185
194
|
}
|
|
195
|
+
await clearStaleChatGptConversationCookies(Network, Target, logger, {
|
|
196
|
+
preserveConversationIds: [
|
|
197
|
+
runtime.conversationId,
|
|
198
|
+
extractConversationIdFromUrl(runtime.tabUrl ?? ""),
|
|
199
|
+
extractConversationIdFromUrl(resolved.url),
|
|
200
|
+
],
|
|
201
|
+
});
|
|
186
202
|
await navigateToChatGPT(Page, Runtime, CHATGPT_URL, logger);
|
|
187
203
|
await ensureNotBlocked(Runtime, resolved.headless, logger);
|
|
188
204
|
await ensureLoggedIn(Runtime, logger, { appliedCookies });
|
|
@@ -268,7 +284,7 @@ async function readPromptPreviewTurnIndex(Runtime, promptPreview) {
|
|
|
268
284
|
const needle = ${JSON.stringify(preview.toLowerCase().replace(/\s+/g, " ").slice(0, 120))};
|
|
269
285
|
if (!needle) return null;
|
|
270
286
|
const normalize = (value) => String(value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
271
|
-
const turns =
|
|
287
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
272
288
|
let matched = null;
|
|
273
289
|
for (const [index, node] of turns.entries()) {
|
|
274
290
|
const attr = (node.getAttribute('data-message-author-role') || node.getAttribute('data-turn') || node.dataset?.turn || '').toLowerCase();
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
|
+
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
2
3
|
import { delay } from "./utils.js";
|
|
3
4
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
4
5
|
export function pickTarget(targets, runtime) {
|
|
5
6
|
if (!Array.isArray(targets) || targets.length === 0) {
|
|
6
7
|
return undefined;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const conversationId = runtime.conversationId ?? extractConversationIdFromUrl(runtime.tabUrl ?? "");
|
|
10
|
+
const byId = runtime.chromeTargetId
|
|
11
|
+
? targets.find((target) => (target.targetId ?? target.id) === runtime.chromeTargetId)
|
|
12
|
+
: undefined;
|
|
13
|
+
if (conversationId) {
|
|
14
|
+
if (byId && extractConversationIdFromUrl(byId.url ?? "") === conversationId) {
|
|
11
15
|
return byId;
|
|
16
|
+
}
|
|
17
|
+
const byConversation = targets.find((target) => extractConversationIdFromUrl(target.url ?? "") === conversationId);
|
|
18
|
+
if (byConversation)
|
|
19
|
+
return byConversation;
|
|
12
20
|
}
|
|
21
|
+
if (byId)
|
|
22
|
+
return byId;
|
|
13
23
|
if (runtime.tabUrl) {
|
|
14
24
|
const byUrl = targets.find((t) => t.url?.startsWith(runtime.tabUrl)) ||
|
|
15
25
|
targets.find((t) => runtime.tabUrl.startsWith(t.url || ""));
|
|
@@ -253,10 +263,9 @@ export async function waitForLocationChange(Runtime, timeoutMs) {
|
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
export async function readConversationTurnIndex(Runtime, logger) {
|
|
256
|
-
const selectorLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
|
|
257
266
|
try {
|
|
258
267
|
const { result } = await Runtime.evaluate({
|
|
259
|
-
expression:
|
|
268
|
+
expression: buildConversationTurnCountExpression(),
|
|
260
269
|
returnByValue: true,
|
|
261
270
|
});
|
|
262
271
|
const raw = typeof result?.value === "number" ? result.value : Number(result?.value);
|
|
@@ -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") {
|