@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
|
@@ -1,7 +1,9 @@
|
|
|
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";
|
|
6
|
+
import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
|
|
5
7
|
import { delay } from "./utils.js";
|
|
6
8
|
export const DEFAULT_REMOTE_CHROME_HOST = "127.0.0.1";
|
|
7
9
|
export const DEFAULT_REMOTE_CHROME_PORT = 9222;
|
|
@@ -58,7 +60,6 @@ function buildTabInspectionExpression() {
|
|
|
58
60
|
const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
|
|
59
61
|
const sendSelectorsLiteral = JSON.stringify(SEND_BUTTON_SELECTORS);
|
|
60
62
|
const answerSelectorsLiteral = JSON.stringify(ANSWER_SELECTORS);
|
|
61
|
-
const turnSelectorLiteral = escapeLiteral(CONVERSATION_TURN_SELECTOR);
|
|
62
63
|
const assistantRoleLiteral = escapeLiteral(ASSISTANT_ROLE_SELECTOR);
|
|
63
64
|
const modelButtonSelectorLiteral = escapeLiteral(MODEL_BUTTON_SELECTOR);
|
|
64
65
|
const stopSelectorLiteral = escapeLiteral(STOP_BUTTON_SELECTOR);
|
|
@@ -66,7 +67,6 @@ function buildTabInspectionExpression() {
|
|
|
66
67
|
const INPUT_SELECTORS = ${inputSelectorsLiteral};
|
|
67
68
|
const SEND_SELECTORS = ${sendSelectorsLiteral};
|
|
68
69
|
const ANSWER_SELECTORS = ${answerSelectorsLiteral};
|
|
69
|
-
const TURN_SELECTOR = ${turnSelectorLiteral};
|
|
70
70
|
const ASSISTANT_ROLE_SELECTOR = ${assistantRoleLiteral};
|
|
71
71
|
const MODEL_BUTTON_SELECTOR = ${modelButtonSelectorLiteral};
|
|
72
72
|
const STOP_BUTTON_SELECTOR = ${stopSelectorLiteral};
|
|
@@ -97,15 +97,21 @@ function buildTabInspectionExpression() {
|
|
|
97
97
|
const sendExists = Boolean(sendButton);
|
|
98
98
|
const promptNode = firstVisible(INPUT_SELECTORS);
|
|
99
99
|
const promptReady = Boolean(promptNode);
|
|
100
|
-
const turns =
|
|
100
|
+
const turns = ${buildConversationTurnListExpression()};
|
|
101
101
|
const assistantTurns = turns.filter((turn) => {
|
|
102
102
|
const role = normalize(turn.getAttribute('data-message-author-role') || turn.getAttribute('data-turn')).toLowerCase();
|
|
103
103
|
if (role === 'assistant') return true;
|
|
104
104
|
return Boolean(turn.querySelector(ASSISTANT_ROLE_SELECTOR));
|
|
105
105
|
});
|
|
106
|
+
const fallbackUserTurns = Array.from(
|
|
107
|
+
document.querySelectorAll('[data-message-author-role="user"], [data-turn="user"]'),
|
|
108
|
+
);
|
|
106
109
|
const userTurns = turns.filter((turn) => {
|
|
107
110
|
const role = normalize(turn.getAttribute('data-message-author-role') || turn.getAttribute('data-turn')).toLowerCase();
|
|
108
|
-
|
|
111
|
+
if (role === 'user') return true;
|
|
112
|
+
return Boolean(
|
|
113
|
+
turn.querySelector('[data-message-author-role="user"], [data-turn="user"]'),
|
|
114
|
+
);
|
|
109
115
|
});
|
|
110
116
|
const answerNode = ANSWER_SELECTORS
|
|
111
117
|
.map((selector) => document.querySelectorAll(selector))
|
|
@@ -116,16 +122,47 @@ function buildTabInspectionExpression() {
|
|
|
116
122
|
if (currentModelLabel === 'ChatGPT' && hasProPill) {
|
|
117
123
|
currentModelLabel = 'ChatGPT + Pro';
|
|
118
124
|
}
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
.
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
125
|
+
const rawAnswerNodes = Array.from(answerNode || []);
|
|
126
|
+
const userCandidates = Array.from(new Set([...userTurns, ...fallbackUserTurns]));
|
|
127
|
+
const lastUserTurn = userCandidates.reduce((latest, candidate) => {
|
|
128
|
+
if (!latest) return candidate;
|
|
129
|
+
return latest.compareDocumentPosition(candidate) & 4 ? candidate : latest;
|
|
130
|
+
}, null);
|
|
131
|
+
const lastUserContainer = lastUserTurn
|
|
132
|
+
? turns.find((turn) => turn === lastUserTurn || turn.contains?.(lastUserTurn))
|
|
133
|
+
: null;
|
|
134
|
+
const answerNodes = rawAnswerNodes.filter(
|
|
135
|
+
(node) =>
|
|
136
|
+
!lastUserTurn ||
|
|
137
|
+
(node !== lastUserTurn &&
|
|
138
|
+
!lastUserTurn.contains?.(node) &&
|
|
139
|
+
!node.contains?.(lastUserTurn)),
|
|
140
|
+
);
|
|
141
|
+
const answerTexts = answerNodes.map((node) => normalize(node.textContent)).filter(Boolean);
|
|
142
|
+
const assistantCandidates = Array.from(new Set([...assistantTurns, ...answerNodes]));
|
|
143
|
+
const lastAssistantNode = assistantCandidates.reduce((latest, candidate) => {
|
|
144
|
+
if (!latest) return candidate;
|
|
145
|
+
return latest.compareDocumentPosition(candidate) & 4 ? candidate : latest;
|
|
146
|
+
}, null);
|
|
147
|
+
const lastAssistantContainer = lastAssistantNode
|
|
148
|
+
? turns.find((turn) => turn === lastAssistantNode || turn.contains?.(lastAssistantNode))
|
|
149
|
+
: null;
|
|
150
|
+
const assistantFollowsLatestUser = Boolean(
|
|
151
|
+
lastAssistantNode &&
|
|
152
|
+
lastUserTurn &&
|
|
153
|
+
lastAssistantNode !== lastUserTurn &&
|
|
154
|
+
(lastUserTurn.compareDocumentPosition(lastAssistantNode) & 4),
|
|
155
|
+
);
|
|
156
|
+
const lastAssistantTurnIndex = lastAssistantContainer
|
|
157
|
+
? turns.indexOf(lastAssistantContainer)
|
|
158
|
+
: -1;
|
|
159
|
+
const lastUserTurnIndex = lastUserContainer ? turns.indexOf(lastUserContainer) : -1;
|
|
160
|
+
const assistantOwners = assistantCandidates.map(
|
|
161
|
+
(node) => turns.find((turn) => turn === node || turn.contains?.(node)) || node,
|
|
162
|
+
);
|
|
163
|
+
const assistantCount = new Set(assistantOwners).size;
|
|
164
|
+
const lastAssistantText = normalize(lastAssistantNode?.textContent);
|
|
165
|
+
const lastUserText = normalize(lastUserTurn?.textContent);
|
|
129
166
|
const authenticated = !loginButtonExists && (promptReady || sendExists || stopExists || assistantCount > 0);
|
|
130
167
|
return {
|
|
131
168
|
title: normalize(document.title),
|
|
@@ -138,12 +175,18 @@ function buildTabInspectionExpression() {
|
|
|
138
175
|
authenticated,
|
|
139
176
|
assistantCount,
|
|
140
177
|
lastAssistantText,
|
|
178
|
+
assistantFollowsLatestUser,
|
|
179
|
+
lastAssistantTurnIndex,
|
|
180
|
+
lastUserTurnIndex,
|
|
141
181
|
lastUserText,
|
|
142
182
|
visibilityState: document.visibilityState,
|
|
143
183
|
focused: Boolean(document.hasFocus?.()),
|
|
144
184
|
};
|
|
145
185
|
})()`;
|
|
146
186
|
}
|
|
187
|
+
export function buildTabInspectionExpressionForTest() {
|
|
188
|
+
return buildTabInspectionExpression();
|
|
189
|
+
}
|
|
147
190
|
export async function listChatGptTargets(options = {}) {
|
|
148
191
|
const { host, port } = normalizeHostPort(options);
|
|
149
192
|
const targets = (await CDP.List({ host, port }));
|
|
@@ -183,7 +226,20 @@ export async function inspectChatGptTab(options) {
|
|
|
183
226
|
});
|
|
184
227
|
const info = (evaluation.result?.value ?? {});
|
|
185
228
|
const snapshot = await readAssistantSnapshot(Runtime).catch(() => null);
|
|
186
|
-
const
|
|
229
|
+
const inspectedAssistantTurnIndex = typeof info.lastAssistantTurnIndex === "number" && info.lastAssistantTurnIndex >= 0
|
|
230
|
+
? info.lastAssistantTurnIndex
|
|
231
|
+
: undefined;
|
|
232
|
+
const normalizedSnapshotText = normalizeTitle(snapshot?.text ?? "").toLowerCase();
|
|
233
|
+
const normalizedInspectedText = normalizeTitle(info.lastAssistantText ?? "").toLowerCase();
|
|
234
|
+
const snapshotMatchesInspectedTurn = (typeof snapshot?.turnIndex === "number" &&
|
|
235
|
+
snapshot.turnIndex === inspectedAssistantTurnIndex) ||
|
|
236
|
+
(snapshot?.turnIndex == null &&
|
|
237
|
+
inspectedAssistantTurnIndex === undefined &&
|
|
238
|
+
normalizedSnapshotText.length > 0 &&
|
|
239
|
+
normalizedSnapshotText === normalizedInspectedText);
|
|
240
|
+
const lastAssistantText = snapshotMatchesInspectedTurn &&
|
|
241
|
+
typeof snapshot?.text === "string" &&
|
|
242
|
+
snapshot.text.trim().length > 0
|
|
187
243
|
? snapshot.text.trim()
|
|
188
244
|
: String(info.lastAssistantText ?? "").trim();
|
|
189
245
|
const lastUserText = String(info.lastUserText ?? "").trim();
|
|
@@ -201,6 +257,11 @@ export async function inspectChatGptTab(options) {
|
|
|
201
257
|
authenticated: Boolean(info.authenticated),
|
|
202
258
|
assistantCount: Number.isFinite(info.assistantCount) ? Number(info.assistantCount) : 0,
|
|
203
259
|
lastAssistantText,
|
|
260
|
+
assistantFollowsLatestUser: Boolean(info.assistantFollowsLatestUser),
|
|
261
|
+
lastAssistantTurnIndex: inspectedAssistantTurnIndex,
|
|
262
|
+
lastUserTurnIndex: typeof info.lastUserTurnIndex === "number" && info.lastUserTurnIndex >= 0
|
|
263
|
+
? info.lastUserTurnIndex
|
|
264
|
+
: undefined,
|
|
204
265
|
lastAssistantSnippet: trimToSnippet(lastAssistantText),
|
|
205
266
|
lastUserText,
|
|
206
267
|
lastUserSnippet: trimToSnippet(lastUserText),
|
|
@@ -210,8 +271,12 @@ export async function inspectChatGptTab(options) {
|
|
|
210
271
|
fingerprint: "",
|
|
211
272
|
state: "detached",
|
|
212
273
|
lastAssistantMarkdown: null,
|
|
213
|
-
lastAssistantMessageId: typeof snapshot?.messageId === "string"
|
|
214
|
-
|
|
274
|
+
lastAssistantMessageId: snapshotMatchesInspectedTurn && typeof snapshot?.messageId === "string"
|
|
275
|
+
? snapshot.messageId
|
|
276
|
+
: undefined,
|
|
277
|
+
lastAssistantTurnId: snapshotMatchesInspectedTurn && typeof snapshot?.turnId === "string"
|
|
278
|
+
? snapshot.turnId
|
|
279
|
+
: undefined,
|
|
215
280
|
};
|
|
216
281
|
summary.state = classifyTabState(summary);
|
|
217
282
|
summary.fingerprint = buildTargetFingerprint(summary);
|
|
@@ -292,6 +357,10 @@ function resolveChatGptTabFromSummaries(summaries, ref) {
|
|
|
292
357
|
if (exactUrl) {
|
|
293
358
|
return exactUrl;
|
|
294
359
|
}
|
|
360
|
+
const exactConversationId = summaries.find((tab) => tab.conversationId === trimmedRef);
|
|
361
|
+
if (exactConversationId) {
|
|
362
|
+
return exactConversationId;
|
|
363
|
+
}
|
|
295
364
|
const lower = trimmedRef.toLowerCase();
|
|
296
365
|
const titleMatches = summaries.filter((tab) => tab.title.toLowerCase().includes(lower));
|
|
297
366
|
if (titleMatches.length === 1) {
|
|
@@ -328,17 +397,6 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
328
397
|
try {
|
|
329
398
|
const { Runtime } = client;
|
|
330
399
|
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
400
|
const nowSummary = await inspectChatGptTab({
|
|
343
401
|
host,
|
|
344
402
|
port,
|
|
@@ -349,15 +407,37 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
349
407
|
type: "page",
|
|
350
408
|
},
|
|
351
409
|
});
|
|
410
|
+
const normalizedSnapshotText = normalizeTitle(snapshot?.text ?? "").toLowerCase();
|
|
411
|
+
const normalizedInspectedText = normalizeTitle(nowSummary.lastAssistantText).toLowerCase();
|
|
412
|
+
const snapshotMatchesLatestTurn = (typeof snapshot?.turnIndex === "number" &&
|
|
413
|
+
snapshot.turnIndex === nowSummary.lastAssistantTurnIndex) ||
|
|
414
|
+
(snapshot?.turnIndex == null &&
|
|
415
|
+
nowSummary.lastAssistantTurnIndex === undefined &&
|
|
416
|
+
normalizedSnapshotText.length > 0 &&
|
|
417
|
+
normalizedSnapshotText === normalizedInspectedText);
|
|
418
|
+
let assistantMarkdown = null;
|
|
419
|
+
if (snapshotMatchesLatestTurn && (snapshot?.messageId || snapshot?.turnId)) {
|
|
420
|
+
assistantMarkdown = await captureAssistantMarkdown(Runtime, {
|
|
421
|
+
messageId: snapshot.messageId,
|
|
422
|
+
turnId: snapshot.turnId,
|
|
423
|
+
}, noopLogger).catch(() => null);
|
|
424
|
+
}
|
|
425
|
+
const lastAssistantText = snapshotMatchesLatestTurn &&
|
|
426
|
+
typeof snapshot?.text === "string" &&
|
|
427
|
+
snapshot.text.trim().length > 0
|
|
428
|
+
? snapshot.text.trim()
|
|
429
|
+
: nowSummary.lastAssistantText;
|
|
352
430
|
const harvested = {
|
|
353
431
|
...nowSummary,
|
|
354
432
|
lastAssistantText,
|
|
355
433
|
lastAssistantSnippet: trimToSnippet(lastAssistantText),
|
|
356
434
|
lastAssistantMarkdown: assistantMarkdown ?? (lastAssistantText || null),
|
|
357
|
-
lastAssistantMessageId: typeof snapshot?.messageId === "string"
|
|
435
|
+
lastAssistantMessageId: snapshotMatchesLatestTurn && typeof snapshot?.messageId === "string"
|
|
358
436
|
? snapshot.messageId
|
|
359
437
|
: nowSummary.lastAssistantMessageId,
|
|
360
|
-
lastAssistantTurnId: typeof snapshot?.turnId === "string"
|
|
438
|
+
lastAssistantTurnId: snapshotMatchesLatestTurn && typeof snapshot?.turnId === "string"
|
|
439
|
+
? snapshot.turnId
|
|
440
|
+
: nowSummary.lastAssistantTurnId,
|
|
361
441
|
};
|
|
362
442
|
if (harvested.stopExists && options.stallWindowMs && options.stallWindowMs > 0) {
|
|
363
443
|
const firstFingerprint = harvested.fingerprint;
|
|
@@ -383,6 +463,9 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
383
463
|
harvested.loginButtonExists = followup.loginButtonExists;
|
|
384
464
|
harvested.lastUserText = followup.lastUserText;
|
|
385
465
|
harvested.lastUserSnippet = followup.lastUserSnippet;
|
|
466
|
+
harvested.assistantFollowsLatestUser = followup.assistantFollowsLatestUser;
|
|
467
|
+
harvested.lastAssistantTurnIndex = followup.lastAssistantTurnIndex;
|
|
468
|
+
harvested.lastUserTurnIndex = followup.lastUserTurnIndex;
|
|
386
469
|
harvested.fingerprint = followup.fingerprint;
|
|
387
470
|
harvested.state =
|
|
388
471
|
harvested.stopExists && firstFingerprint === followup.fingerprint
|
|
@@ -399,8 +482,7 @@ export async function harvestChatGptTab(options = {}) {
|
|
|
399
482
|
}
|
|
400
483
|
}
|
|
401
484
|
export function extractConversationIdFromUrl(url) {
|
|
402
|
-
|
|
403
|
-
return match?.[1] ?? undefined;
|
|
485
|
+
return extractStableConversationIdFromUrl(normalizeUrl(url));
|
|
404
486
|
}
|
|
405
487
|
export function formatBrowserTabState(tab) {
|
|
406
488
|
return tab.state ?? classifyTabState(tab);
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { CONVERSATION_TURN_SELECTOR } from "./constants.js";
|
|
2
2
|
import { buildConversationTurnCountExpression } from "./conversationTurns.js";
|
|
3
|
+
import { extractStableConversationIdFromUrl } from "./conversationUrl.js";
|
|
3
4
|
import { delay } from "./utils.js";
|
|
4
5
|
import { readAssistantSnapshot } from "./pageActions.js";
|
|
5
6
|
export function pickTarget(targets, runtime) {
|
|
@@ -29,14 +30,11 @@ export function pickTarget(targets, runtime) {
|
|
|
29
30
|
return targets.find((t) => t.type === "page") ?? targets[0];
|
|
30
31
|
}
|
|
31
32
|
export function extractConversationIdFromUrl(url) {
|
|
32
|
-
|
|
33
|
-
return undefined;
|
|
34
|
-
const match = url.match(/\/c\/([a-zA-Z0-9-]+)/);
|
|
35
|
-
return match?.[1];
|
|
33
|
+
return extractStableConversationIdFromUrl(url);
|
|
36
34
|
}
|
|
37
35
|
export function buildConversationUrl(runtime, baseUrl) {
|
|
38
36
|
if (runtime.tabUrl) {
|
|
39
|
-
if (runtime.tabUrl
|
|
37
|
+
if (extractConversationIdFromUrl(runtime.tabUrl)) {
|
|
40
38
|
return runtime.tabUrl;
|
|
41
39
|
}
|
|
42
40
|
return null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isStableConversationUrl } from "./conversationUrl.js";
|
|
1
2
|
/**
|
|
2
3
|
* True when the URL points at a specific ChatGPT conversation (`/c/<id>`) on
|
|
3
4
|
* chatgpt.com or chat.openai.com. Rejects home, project shell, and external
|
|
@@ -17,7 +18,7 @@ export function isRecoverableChatGptConversationUrl(candidate) {
|
|
|
17
18
|
if (url.hostname !== "chatgpt.com" && url.hostname !== "chat.openai.com") {
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
20
|
-
return
|
|
21
|
+
return isStableConversationUrl(url.pathname);
|
|
21
22
|
}
|
|
22
23
|
catch {
|
|
23
24
|
return false;
|
|
@@ -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
|
}
|
|
@@ -61,7 +61,7 @@ export async function acquireBrowserTabLease(profileDir, options, deps = {}) {
|
|
|
61
61
|
options.logger?.(`[browser] Acquired ChatGPT browser slot ${leaseId.slice(0, 8)} (${maxConcurrentTabs} max).`);
|
|
62
62
|
return {
|
|
63
63
|
id: leaseId,
|
|
64
|
-
release: async () => releaseBrowserTabLease(profileDir, leaseId, options.logger),
|
|
64
|
+
release: async (releaseOptions) => releaseBrowserTabLease(profileDir, leaseId, options.logger, releaseOptions),
|
|
65
65
|
update: async (patch) => updateBrowserTabLease(profileDir, leaseId, patch),
|
|
66
66
|
};
|
|
67
67
|
}
|
|
@@ -86,11 +86,17 @@ export async function updateBrowserTabLease(profileDir, leaseId, patch) {
|
|
|
86
86
|
await writeRegistry(profileDir, { version: 1, leases });
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
|
-
export async function releaseBrowserTabLease(profileDir, leaseId, logger) {
|
|
89
|
+
export async function releaseBrowserTabLease(profileDir, leaseId, logger, options = {}) {
|
|
90
90
|
await withRegistryLock(profileDir, async () => {
|
|
91
91
|
const registry = await readRegistry(profileDir);
|
|
92
|
-
const
|
|
92
|
+
const active = pruneStaleLeases(registry.leases, {
|
|
93
|
+
nowMs: Date.now(),
|
|
94
|
+
staleMs: DEFAULT_STALE_MS,
|
|
95
|
+
isProcessAlive,
|
|
96
|
+
});
|
|
97
|
+
const leases = active.filter((lease) => lease.id !== leaseId);
|
|
93
98
|
await writeRegistry(profileDir, { version: 1, leases });
|
|
99
|
+
await options.onRelease?.({ isLastLease: leases.length === 0 });
|
|
94
100
|
}).catch(() => undefined);
|
|
95
101
|
logger?.(`[browser] Released ChatGPT browser slot ${leaseId.slice(0, 8)}.`);
|
|
96
102
|
}
|
|
@@ -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") {
|
|
@@ -11,6 +11,7 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
11
11
|
};
|
|
12
12
|
const attachRunningRequested = options.browserAttachRunning === true ||
|
|
13
13
|
(isUnset("browserAttachRunning") && browser.attachRunning === true);
|
|
14
|
+
const currentModelRequestedByCli = options.browserModelStrategy === "current" && getSource("browserModelStrategy") === "cli";
|
|
14
15
|
const configuredChatgptUrl = browser.chatgptUrl ?? browser.url;
|
|
15
16
|
const cliChatgptSet = options.chatgptUrl !== undefined || options.browserUrl !== undefined;
|
|
16
17
|
if (isUnset("chatgptUrl") && !cliChatgptSet && configuredChatgptUrl !== undefined) {
|
|
@@ -89,7 +90,9 @@ export function applyBrowserDefaultsFromConfig(options, config, getSource) {
|
|
|
89
90
|
if (isUnset("browserModelStrategy") && browser.modelStrategy !== undefined) {
|
|
90
91
|
options.browserModelStrategy = browser.modelStrategy;
|
|
91
92
|
}
|
|
92
|
-
if (
|
|
93
|
+
if (!currentModelRequestedByCli &&
|
|
94
|
+
isUnset("browserThinkingTime") &&
|
|
95
|
+
browser.thinkingTime !== undefined) {
|
|
93
96
|
options.browserThinkingTime = normalizeThinkingTimeLevel(browser.thinkingTime) ?? undefined;
|
|
94
97
|
}
|
|
95
98
|
if (isUnset("browserResearch") && browser.researchMode !== undefined) {
|