@steipete/oracle 0.11.0 → 0.12.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/README.md +56 -11
- package/dist/bin/oracle-cli.js +440 -98
- package/dist/src/browser/actions/archiveConversation.js +12 -0
- package/dist/src/browser/actions/modelSelection.js +61 -18
- package/dist/src/browser/actions/navigation.js +5 -3
- package/dist/src/browser/actions/promptComposer.js +75 -18
- package/dist/src/browser/actions/thinkingTime.js +23 -8
- package/dist/src/browser/config.js +1 -7
- package/dist/src/browser/constants.js +1 -1
- package/dist/src/browser/index.js +65 -48
- package/dist/src/browser/manualLoginProfile.js +54 -0
- package/dist/src/browser/projectSourcesRunner.js +16 -5
- package/dist/src/browser/prompt.js +56 -37
- package/dist/src/browser/sessionRunner.js +72 -1
- package/dist/src/browser/utils.js +1 -47
- package/dist/src/browser/zipBundle.js +152 -0
- package/dist/src/cli/browserConfig.js +13 -18
- package/dist/src/cli/browserDefaults.js +2 -1
- package/dist/src/cli/docsCheck.js +186 -0
- package/dist/src/cli/engine.js +11 -4
- package/dist/src/cli/options.js +12 -6
- package/dist/src/cli/perfTrace.js +242 -0
- package/dist/src/cli/promptRequirement.js +2 -0
- package/dist/src/cli/providerDoctor.js +85 -0
- package/dist/src/cli/runOptions.js +46 -16
- package/dist/src/cli/sessionDisplay.js +39 -4
- package/dist/src/cli/sessionLifecycle.js +38 -0
- package/dist/src/cli/sessionRunner.js +228 -3
- package/dist/src/cli/sessionTable.js +2 -1
- package/dist/src/duration.js +47 -0
- package/dist/src/mcp/tools/consult.js +19 -3
- package/dist/src/mcp/types.js +5 -2
- package/dist/src/mcp/utils.js +4 -1
- package/dist/src/oracle/baseUrl.js +17 -0
- package/dist/src/oracle/client.js +1 -22
- package/dist/src/oracle/config.js +17 -4
- package/dist/src/oracle/gemini.js +2 -22
- package/dist/src/oracle/geminiModels.js +21 -0
- package/dist/src/oracle/modelResolver.js +7 -1
- package/dist/src/oracle/multiModelRunner.js +20 -2
- package/dist/src/oracle/providerFailures.js +204 -0
- package/dist/src/oracle/providerRoutePlan.js +281 -0
- package/dist/src/oracle/providerRouting.js +92 -0
- package/dist/src/oracle/run.js +157 -54
- package/dist/src/oracle.js +1 -0
- package/dist/src/remote/client.js +8 -0
- package/dist/src/remote/server.js +26 -0
- package/dist/src/sessionManager.js +5 -1
- package/package.json +8 -6
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
export function isProjectChatgptUrl(url) {
|
|
2
2
|
return /\/project(?:[/?#]|$)/i.test(url ?? "");
|
|
3
3
|
}
|
|
4
|
+
export function isTemporaryChatgptUrl(url) {
|
|
5
|
+
try {
|
|
6
|
+
const parsed = new URL(url ?? "");
|
|
7
|
+
return (parsed.searchParams.get("temporary-chat") ?? "").trim().toLowerCase() === "true";
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
4
13
|
export function resolveBrowserArchiveDecision({ mode = "auto", chatgptUrl, conversationUrl, researchMode, followUpCount, }) {
|
|
5
14
|
if (mode === "never") {
|
|
6
15
|
return { mode, shouldArchive: false, reason: "disabled" };
|
|
@@ -8,6 +17,9 @@ export function resolveBrowserArchiveDecision({ mode = "auto", chatgptUrl, conve
|
|
|
8
17
|
if (!conversationUrl) {
|
|
9
18
|
return { mode, shouldArchive: false, reason: "missing-conversation-url" };
|
|
10
19
|
}
|
|
20
|
+
if (isTemporaryChatgptUrl(chatgptUrl) || isTemporaryChatgptUrl(conversationUrl)) {
|
|
21
|
+
return { mode, shouldArchive: false, reason: "temporary-chat" };
|
|
22
|
+
}
|
|
11
23
|
if (mode === "always") {
|
|
12
24
|
return { mode, shouldArchive: true, reason: "forced" };
|
|
13
25
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { COMPOSER_MODEL_SIGNAL_SELECTOR, MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
|
|
2
2
|
import { logDomFailure } from "../domDebug.js";
|
|
3
3
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
4
|
+
const LEGACY_PRO_VERSION_WORD_TOKENS = ["5 4", "5 2", "5 1", "5 0", "gpt 5 pro"];
|
|
5
|
+
const LEGACY_PRO_VERSION_COMPACT_TOKENS = ["gpt54", "gpt52", "gpt51", "gpt50"];
|
|
4
6
|
export async function ensureModelSelection(Runtime, desiredModel, logger, strategy = "select") {
|
|
5
7
|
const outcome = await Runtime.evaluate({
|
|
6
8
|
expression: buildModelSelectionExpression(desiredModel, strategy),
|
|
@@ -17,7 +19,15 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
17
19
|
assertResolvedModelSelection(desiredModel, label);
|
|
18
20
|
}
|
|
19
21
|
logger(`Model picker: ${label}`);
|
|
20
|
-
return
|
|
22
|
+
return {
|
|
23
|
+
requestedModel: desiredModel,
|
|
24
|
+
resolvedLabel: label,
|
|
25
|
+
strategy,
|
|
26
|
+
status: result.status,
|
|
27
|
+
verified: strategy !== "current",
|
|
28
|
+
source: "chatgpt-model-picker",
|
|
29
|
+
capturedAt: new Date().toISOString(),
|
|
30
|
+
};
|
|
21
31
|
}
|
|
22
32
|
case "option-not-found": {
|
|
23
33
|
await logDomFailure(Runtime, logger, "model-switcher-option");
|
|
@@ -25,7 +35,7 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
25
35
|
const available = (result.hint?.availableOptions ?? []).filter(Boolean);
|
|
26
36
|
const availableHint = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
|
|
27
37
|
const tempHint = isTemporary && /\bpro\b/i.test(desiredModel)
|
|
28
|
-
?
|
|
38
|
+
? " You are in Temporary Chat mode; model labels may differ there. If the current Temporary Chat already shows the desired Pro mode, retry with --browser-model-strategy current; otherwise choose an available model or turn Temporary Chat off."
|
|
29
39
|
: "";
|
|
30
40
|
throw new Error(`Unable to find model option matching "${desiredModel}" in the model switcher.${availableHint}${tempHint}`);
|
|
31
41
|
}
|
|
@@ -38,22 +48,39 @@ export async function ensureModelSelection(Runtime, desiredModel, logger, strate
|
|
|
38
48
|
function assertResolvedModelSelection(desiredModel, resolvedLabel) {
|
|
39
49
|
const desired = desiredModel.toLowerCase();
|
|
40
50
|
const resolved = resolvedLabel.toLowerCase();
|
|
41
|
-
const wantsGpt55Pro = desired === "
|
|
51
|
+
const wantsGpt55Pro = desired === "pro" ||
|
|
52
|
+
desired === "chatgpt pro" ||
|
|
53
|
+
desired === "gpt-5.5-pro" ||
|
|
42
54
|
desired.includes("5.5 pro") ||
|
|
43
55
|
desired.includes("5-5 pro") ||
|
|
44
56
|
(desired.includes("pro") && desired.includes("extended"));
|
|
45
57
|
if (!wantsGpt55Pro || !resolved) {
|
|
46
58
|
return;
|
|
47
59
|
}
|
|
48
|
-
|
|
60
|
+
if (!hasCurrentProSignal(resolved) ||
|
|
61
|
+
hasLegacyProVersionLabel(resolved) ||
|
|
62
|
+
(resolved.includes("thinking") && !resolved.includes("pro"))) {
|
|
63
|
+
throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-5.5 Pro. Use model "gpt-5.5" with browser thinking time for the Thinking variant.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function normalizeResolvedModelLabel(value) {
|
|
67
|
+
return value
|
|
68
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
69
|
+
.replace(/\s+/g, " ")
|
|
70
|
+
.trim();
|
|
71
|
+
}
|
|
72
|
+
function hasCurrentProSignal(resolved) {
|
|
73
|
+
return (resolved.includes(" pro") ||
|
|
49
74
|
resolved.endsWith("pro") ||
|
|
50
75
|
resolved.includes("pro ") ||
|
|
51
76
|
resolved.includes("extended") ||
|
|
52
77
|
resolved.includes("gpt-5.5-pro") ||
|
|
53
|
-
resolved.includes("gpt 5 5 pro");
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
78
|
+
resolved.includes("gpt 5 5 pro"));
|
|
79
|
+
}
|
|
80
|
+
function hasLegacyProVersionLabel(resolved) {
|
|
81
|
+
const normalized = normalizeResolvedModelLabel(resolved);
|
|
82
|
+
return (LEGACY_PRO_VERSION_WORD_TOKENS.some((token) => normalized.includes(token)) ||
|
|
83
|
+
LEGACY_PRO_VERSION_COMPACT_TOKENS.some((token) => resolved.includes(token)));
|
|
57
84
|
}
|
|
58
85
|
export function assertResolvedModelSelectionForTest(desiredModel, resolvedLabel) {
|
|
59
86
|
assertResolvedModelSelection(desiredModel, resolvedLabel);
|
|
@@ -121,14 +148,27 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
121
148
|
const wantsPro = normalizedTarget.includes(' pro') || normalizedTarget.endsWith(' pro') || normalizedTokens.includes('pro');
|
|
122
149
|
const wantsInstant = normalizedTarget.includes('instant');
|
|
123
150
|
const wantsThinking = normalizedTarget.includes('thinking');
|
|
151
|
+
const targetUsesCurrentGpt55Alias =
|
|
152
|
+
desiredVersion === '5-5' || normalizedTarget === 'pro' || normalizedTarget === 'chatgpt pro';
|
|
153
|
+
const labelHasProWord = (label) => label === 'pro' || label.startsWith('pro ') || label.includes(' pro ') || label.endsWith(' pro');
|
|
154
|
+
const legacyProVersionTokens = ['5 4', '5 2', '5 1', '5 0', 'gpt54', 'gpt52', 'gpt51', 'gpt50', 'gpt 5 pro'];
|
|
155
|
+
const labelHasLegacyProVersion = (value) => {
|
|
156
|
+
const label = normalizeText(value);
|
|
157
|
+
return legacyProVersionTokens.some((token) => label.includes(token));
|
|
158
|
+
};
|
|
124
159
|
const isTargetGpt55VisibleAlias = (value) => {
|
|
125
|
-
if (
|
|
160
|
+
if (!targetUsesCurrentGpt55Alias) return false;
|
|
126
161
|
const label = normalizeText(value);
|
|
127
162
|
if (wantsPro) {
|
|
128
|
-
|
|
163
|
+
// ChatGPT UI as of 2026-05: the picker shows just "Pro" (no longer "Pro Extended").
|
|
164
|
+
// "Extended" is now a thinking-effort sub-setting, not part of the model label.
|
|
165
|
+
// Accept bare "pro", legacy "pro extended", and reversed "extended pro" (composer pill).
|
|
166
|
+
return (label === 'pro' || label === 'pro extended' || label === 'extended pro') && !label.includes('thinking');
|
|
129
167
|
}
|
|
130
168
|
if (wantsThinking) {
|
|
131
|
-
|
|
169
|
+
// ChatGPT UI as of 2026-05: the picker shows "Thinking" or "Thinking · Extended"
|
|
170
|
+
// (normalized to "thinking extended"). Accept both old "thinking heavy" and new labels.
|
|
171
|
+
return (label === 'thinking' || label === 'thinking extended' || label === 'thinking heavy') && !label.includes('pro');
|
|
132
172
|
}
|
|
133
173
|
return false;
|
|
134
174
|
};
|
|
@@ -195,7 +235,8 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
195
235
|
if (desiredVersion === '5-1' && !normalizedLabel.includes('5 1')) return false;
|
|
196
236
|
if (desiredVersion === '5-0' && !normalizedLabel.includes('5 0')) return false;
|
|
197
237
|
}
|
|
198
|
-
if (wantsPro &&
|
|
238
|
+
if (wantsPro && labelHasLegacyProVersion(normalizedLabel)) return false;
|
|
239
|
+
if (wantsPro && !labelHasProWord(normalizedLabel)) return false;
|
|
199
240
|
if (wantsInstant && !normalizedLabel.includes('instant')) return false;
|
|
200
241
|
if (wantsThinking && !normalizedLabel.includes('thinking')) return false;
|
|
201
242
|
// Also reject if button has variants we DON'T want
|
|
@@ -213,6 +254,9 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
213
254
|
if (!signal) {
|
|
214
255
|
return COMPOSER_SIGNAL_ALLOW_BLANK;
|
|
215
256
|
}
|
|
257
|
+
if (wantsPro && labelHasLegacyProVersion(signal)) {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
216
260
|
if (COMPOSER_SIGNAL_EXCLUDES.some((token) => token && signal.includes(token))) {
|
|
217
261
|
return false;
|
|
218
262
|
}
|
|
@@ -351,15 +395,14 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
351
395
|
const candidateGpt55VisibleAlias = isTargetGpt55VisibleAlias(normalizedText);
|
|
352
396
|
const candidateHasThinking =
|
|
353
397
|
normalizedText.includes('thinking') || normalizedTestId.includes('thinking');
|
|
398
|
+
const candidateHasLegacyProVersion = labelHasLegacyProVersion(normalizedText);
|
|
354
399
|
const candidateHasPro =
|
|
355
400
|
candidateGpt55VisibleAlias ||
|
|
356
|
-
normalizedText
|
|
357
|
-
normalizedText.startsWith('pro ') ||
|
|
358
|
-
normalizedText.includes(' pro ') ||
|
|
359
|
-
normalizedText.endsWith(' pro') ||
|
|
401
|
+
labelHasProWord(normalizedText) ||
|
|
360
402
|
normalizedText.includes('proresearch') ||
|
|
361
403
|
normalizedTestId.includes('pro');
|
|
362
404
|
if (wantsPro && candidateHasThinking) return 0;
|
|
405
|
+
if (wantsPro && candidateHasLegacyProVersion) return 0;
|
|
363
406
|
if (wantsPro && !candidateHasPro) return 0;
|
|
364
407
|
if (wantsThinking && candidateHasPro) return 0;
|
|
365
408
|
if (desiredVersion === '5-5' && normalizedText && !candidateGpt55VisibleAlias) {
|
|
@@ -402,10 +445,10 @@ function buildModelSelectionExpression(targetModel, strategy) {
|
|
|
402
445
|
}
|
|
403
446
|
// If the caller didn't explicitly ask for Pro, prefer non-Pro options when both exist.
|
|
404
447
|
if (wantsPro) {
|
|
405
|
-
if (!normalizedText
|
|
448
|
+
if (!labelHasProWord(normalizedText)) {
|
|
406
449
|
score -= 80;
|
|
407
450
|
}
|
|
408
|
-
} else if (normalizedText
|
|
451
|
+
} else if (labelHasProWord(normalizedText)) {
|
|
409
452
|
score -= 40;
|
|
410
453
|
}
|
|
411
454
|
// Similarly for Thinking variant
|
|
@@ -388,9 +388,8 @@ function buildLoginProbeExpression(timeoutMs) {
|
|
|
388
388
|
if (!text) return false;
|
|
389
389
|
const normalized = text.toLowerCase().trim();
|
|
390
390
|
return (
|
|
391
|
-
['log in', 'login', 'sign in', 'signin', '
|
|
392
|
-
|
|
393
|
-
) ||
|
|
391
|
+
['log in', 'login', 'sign in', 'signin', 'sign up for free'].includes(normalized) ||
|
|
392
|
+
normalized.startsWith('continue with') ||
|
|
394
393
|
normalized.includes('get responses tailored to you') ||
|
|
395
394
|
normalized.includes('log in to get answers')
|
|
396
395
|
);
|
|
@@ -490,3 +489,6 @@ function normalizeLoginProbe(raw) {
|
|
|
490
489
|
onAuthPage: Boolean(value.onAuthPage),
|
|
491
490
|
};
|
|
492
491
|
}
|
|
492
|
+
export function buildLoginProbeExpressionForTest(timeoutMs = LOGIN_CHECK_TIMEOUT_MS) {
|
|
493
|
+
return buildLoginProbeExpression(timeoutMs);
|
|
494
|
+
}
|
|
@@ -290,17 +290,35 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
290
290
|
document.querySelector('form') ||
|
|
291
291
|
document.body ||
|
|
292
292
|
document;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
.
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
// Walk node + ancestors (up to grandparent) + descendants to gather every textual hint.
|
|
294
|
+
// ChatGPT's current chip DOM nests the filename inside truncated child spans, so checking
|
|
295
|
+
// only the node's own textContent/aria/title misses the match.
|
|
296
|
+
const collectLabelHaystack = (node) => {
|
|
297
|
+
if (!node) return '';
|
|
298
|
+
const pieces = [];
|
|
299
|
+
const pushAttrs = (el) => {
|
|
300
|
+
if (!el || typeof el.getAttribute !== 'function') return;
|
|
301
|
+
for (const attr of ['aria-label', 'title', 'data-testid', 'data-tooltip', 'data-tooltip-content']) {
|
|
302
|
+
const v = el.getAttribute(attr);
|
|
303
|
+
if (v) pieces.push(v);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const pushText = (el) => {
|
|
307
|
+
if (!el) return;
|
|
308
|
+
const text = (el.innerText ?? el.textContent ?? '').trim();
|
|
309
|
+
if (text) pieces.push(text);
|
|
310
|
+
};
|
|
311
|
+
pushAttrs(node);
|
|
312
|
+
pushText(node);
|
|
313
|
+
const parent = node.parentElement;
|
|
314
|
+
pushAttrs(parent);
|
|
315
|
+
pushText(parent);
|
|
316
|
+
const grandparent = parent?.parentElement;
|
|
317
|
+
pushAttrs(grandparent);
|
|
318
|
+
pushText(grandparent);
|
|
319
|
+
return pieces.join(' ').toLowerCase();
|
|
320
|
+
};
|
|
321
|
+
const match = (node, name) => collectLabelHaystack(node).includes(name);
|
|
304
322
|
|
|
305
323
|
// Restrict to attachment affordances; never scan generic div/span nodes (prompt text can contain the file name).
|
|
306
324
|
const attachmentSelectors = [
|
|
@@ -312,13 +330,32 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
312
330
|
'button[aria-label*="Remove file"]',
|
|
313
331
|
'[aria-label*="remove file"]',
|
|
314
332
|
'button[aria-label*="remove file"]',
|
|
333
|
+
'[aria-label*="Remove attachment"]',
|
|
334
|
+
'button[aria-label*="Remove attachment"]',
|
|
335
|
+
'[aria-label*="remove attachment"]',
|
|
336
|
+
'button[aria-label*="remove attachment"]',
|
|
315
337
|
];
|
|
316
|
-
const attachmentRoots = Array.from(new Set([composer
|
|
338
|
+
const attachmentRoots = Array.from(new Set([composer])).filter(Boolean);
|
|
339
|
+
const collectChipNodes = () => {
|
|
340
|
+
const seen = new Set();
|
|
341
|
+
const collected = [];
|
|
342
|
+
for (const root of attachmentRoots) {
|
|
343
|
+
for (const node of Array.from(root.querySelectorAll(attachmentSelectors.join(',')))) {
|
|
344
|
+
if (!(node instanceof HTMLElement)) continue;
|
|
345
|
+
// Skip elements clearly inside the editable input (composer textarea may contain
|
|
346
|
+
// filename text in the user's prompt — avoid mistaking that for a chip).
|
|
347
|
+
if (node.closest('textarea,[contenteditable="true"]')) continue;
|
|
348
|
+
if (seen.has(node)) continue;
|
|
349
|
+
seen.add(node);
|
|
350
|
+
collected.push(node);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return collected;
|
|
354
|
+
};
|
|
355
|
+
const chipNodes = collectChipNodes();
|
|
317
356
|
|
|
318
357
|
const chipsReady = names.every((name) =>
|
|
319
|
-
|
|
320
|
-
Array.from(root.querySelectorAll(attachmentSelectors.join(','))).some((node) => match(node, name)),
|
|
321
|
-
),
|
|
358
|
+
chipNodes.some((node) => match(node, name)),
|
|
322
359
|
);
|
|
323
360
|
const inputsReady = names.every((name) =>
|
|
324
361
|
attachmentRoots.some((root) =>
|
|
@@ -329,14 +366,28 @@ function buildAttachmentReadyExpression(attachmentNames) {
|
|
|
329
366
|
),
|
|
330
367
|
),
|
|
331
368
|
);
|
|
369
|
+
// Count-based fallback: if we cannot match names individually (ChatGPT may strip
|
|
370
|
+
// the filename out of attribute-readable text into a deeply nested span), but we
|
|
371
|
+
// do see at least as many distinct chip-shaped nodes as attachments we uploaded,
|
|
372
|
+
// and a sibling "Remove" affordance exists per chip, trust the upload.
|
|
373
|
+
const removeAffordanceCount = chipNodes.filter((node) => {
|
|
374
|
+
const aria = (node.getAttribute?.('aria-label') ?? '').toLowerCase();
|
|
375
|
+
if (aria.includes('remove')) return true;
|
|
376
|
+
const removeSibling = node.querySelector?.(
|
|
377
|
+
'[aria-label*="Remove" i], [aria-label*="remove" i], button[aria-label*="Remove" i], button[aria-label*="remove" i]',
|
|
378
|
+
);
|
|
379
|
+
return Boolean(removeSibling);
|
|
380
|
+
}).length;
|
|
381
|
+
const countReady = chipNodes.length >= names.length && removeAffordanceCount >= names.length;
|
|
332
382
|
|
|
333
|
-
return chipsReady || inputsReady;
|
|
383
|
+
return chipsReady || inputsReady || countReady;
|
|
334
384
|
})()`;
|
|
335
385
|
}
|
|
336
386
|
export function buildAttachmentReadyExpressionForTest(attachmentNames) {
|
|
337
387
|
return buildAttachmentReadyExpression(attachmentNames);
|
|
338
388
|
}
|
|
339
389
|
async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
390
|
+
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
340
391
|
const script = `(() => {
|
|
341
392
|
${buildClickDispatcher()}
|
|
342
393
|
const selectors = ${JSON.stringify(SEND_BUTTON_SELECTORS)};
|
|
@@ -369,9 +420,11 @@ async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
|
369
420
|
dispatchClickSequence(button);
|
|
370
421
|
return 'clicked';
|
|
371
422
|
})()`;
|
|
372
|
-
|
|
423
|
+
// Give attachment-bearing submissions more headroom. ChatGPT's chip render can
|
|
424
|
+
// settle slowly for multi-file uploads, but plain text sends should keep the
|
|
425
|
+
// shorter historical deadline.
|
|
426
|
+
const deadline = Date.now() + sendButtonTimeoutMs(attachmentNames);
|
|
373
427
|
while (Date.now() < deadline) {
|
|
374
|
-
const needAttachment = Array.isArray(attachmentNames) && attachmentNames.length > 0;
|
|
375
428
|
if (needAttachment) {
|
|
376
429
|
const ready = await Runtime.evaluate({
|
|
377
430
|
expression: buildAttachmentReadyExpression(attachmentNames),
|
|
@@ -400,6 +453,9 @@ async function attemptSendButton(Runtime, _logger, attachmentNames) {
|
|
|
400
453
|
}
|
|
401
454
|
return false;
|
|
402
455
|
}
|
|
456
|
+
function sendButtonTimeoutMs(attachmentNames) {
|
|
457
|
+
return Array.isArray(attachmentNames) && attachmentNames.length > 0 ? 45_000 : 20_000;
|
|
458
|
+
}
|
|
403
459
|
async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselineTurns) {
|
|
404
460
|
const deadline = Date.now() + timeoutMs;
|
|
405
461
|
const encodedPrompt = JSON.stringify(prompt.trim());
|
|
@@ -542,5 +598,6 @@ async function verifyPromptCommitted(Runtime, prompt, timeoutMs, logger, baselin
|
|
|
542
598
|
// biome-ignore lint/style/useNamingConvention: test-only export used in vitest suite
|
|
543
599
|
export const __test__ = {
|
|
544
600
|
attemptSendButton,
|
|
601
|
+
sendButtonTimeoutMs,
|
|
545
602
|
verifyPromptCommitted,
|
|
546
603
|
};
|
|
@@ -207,21 +207,36 @@ function buildThinkingTimeExpression(level) {
|
|
|
207
207
|
|
|
208
208
|
const findModelButton = () => document.querySelector(MODEL_BUTTON_SELECTOR);
|
|
209
209
|
const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
|
|
210
|
+
const findEffortRow = (node) => {
|
|
211
|
+
let current = node instanceof HTMLElement ? node.parentElement : null;
|
|
212
|
+
while (current && current !== document.body) {
|
|
213
|
+
if (current.getAttribute?.('data-model-picker-thinking-effort-row') === 'true') {
|
|
214
|
+
return current;
|
|
215
|
+
}
|
|
216
|
+
current = current.parentElement;
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
const rowIsSelected = (row) => {
|
|
221
|
+
if (!(row instanceof HTMLElement)) return false;
|
|
222
|
+
const modelItem = row.querySelector('[data-model-picker-thinking-effort-menu-item="true"], [role="menuitemradio"]');
|
|
223
|
+
if (optionIsSelected(modelItem)) return true;
|
|
224
|
+
return Boolean(
|
|
225
|
+
row.querySelector(
|
|
226
|
+
'[aria-checked="true"], [aria-selected="true"], [aria-current="true"], [data-selected="true"], [data-state="checked"], [data-state="selected"], [data-state="on"]',
|
|
227
|
+
),
|
|
228
|
+
);
|
|
229
|
+
};
|
|
210
230
|
const pickTrailingForCurrentModel = () => {
|
|
211
231
|
const trailings = findTrailingButtons();
|
|
212
232
|
if (trailings.length === 0) return null;
|
|
213
233
|
if (trailings.length === 1) return trailings[0];
|
|
214
234
|
// Prefer the trailing button whose model row is currently selected.
|
|
215
235
|
for (const t of trailings) {
|
|
216
|
-
const row = t
|
|
217
|
-
if (
|
|
218
|
-
}
|
|
219
|
-
// Fallback: first one with non-zero box.
|
|
220
|
-
for (const t of trailings) {
|
|
221
|
-
const r = t.getBoundingClientRect?.();
|
|
222
|
-
if (r && r.width > 0 && r.height > 0) return t;
|
|
236
|
+
const row = findEffortRow(t);
|
|
237
|
+
if (rowIsSelected(row)) return t;
|
|
223
238
|
}
|
|
224
|
-
return
|
|
239
|
+
return null;
|
|
225
240
|
};
|
|
226
241
|
|
|
227
242
|
const modelBtn = findModelButton();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CHATGPT_URL, DEEP_RESEARCH_DEFAULT_TIMEOUT_MS, DEFAULT_MODEL_STRATEGY, DEFAULT_MODEL_TARGET, } from "./constants.js";
|
|
2
2
|
import { normalizeBrowserModelStrategy } from "./modelStrategy.js";
|
|
3
3
|
import { DEFAULT_MAX_CONCURRENT_CHATGPT_TABS, normalizeMaxConcurrentTabs, } from "./tabLeaseRegistry.js";
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeChatgptUrl } from "./utils.js";
|
|
5
5
|
import os from "node:os";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
export const DEFAULT_CHATGPT_COOKIE_NAMES = [
|
|
@@ -65,12 +65,6 @@ export function resolveBrowserConfig(config) {
|
|
|
65
65
|
const modelStrategy = normalizeBrowserModelStrategy(config?.modelStrategy) ??
|
|
66
66
|
DEFAULT_BROWSER_CONFIG.modelStrategy ??
|
|
67
67
|
DEFAULT_MODEL_STRATEGY;
|
|
68
|
-
if (modelStrategy === "select" &&
|
|
69
|
-
isTemporaryChatUrl(normalizedUrl) &&
|
|
70
|
-
/\bpro\b/i.test(desiredModel)) {
|
|
71
|
-
throw new Error("Temporary Chat mode does not expose Pro models in the ChatGPT model picker. " +
|
|
72
|
-
'Remove "temporary-chat=true" from your browser URL, or use a non-Pro model label (e.g. "GPT-5.2").');
|
|
73
|
-
}
|
|
74
68
|
const isWindows = process.platform === "win32";
|
|
75
69
|
const manualLogin = config?.manualLogin ?? (isWindows ? true : DEFAULT_BROWSER_CONFIG.manualLogin);
|
|
76
70
|
const cookieSyncDefault = isWindows ? false : DEFAULT_BROWSER_CONFIG.cookieSync;
|