@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.
Files changed (49) hide show
  1. package/README.md +56 -11
  2. package/dist/bin/oracle-cli.js +440 -98
  3. package/dist/src/browser/actions/archiveConversation.js +12 -0
  4. package/dist/src/browser/actions/modelSelection.js +61 -18
  5. package/dist/src/browser/actions/navigation.js +5 -3
  6. package/dist/src/browser/actions/promptComposer.js +75 -18
  7. package/dist/src/browser/actions/thinkingTime.js +23 -8
  8. package/dist/src/browser/config.js +1 -7
  9. package/dist/src/browser/constants.js +1 -1
  10. package/dist/src/browser/index.js +65 -48
  11. package/dist/src/browser/manualLoginProfile.js +54 -0
  12. package/dist/src/browser/projectSourcesRunner.js +16 -5
  13. package/dist/src/browser/prompt.js +56 -37
  14. package/dist/src/browser/sessionRunner.js +72 -1
  15. package/dist/src/browser/utils.js +1 -47
  16. package/dist/src/browser/zipBundle.js +152 -0
  17. package/dist/src/cli/browserConfig.js +13 -18
  18. package/dist/src/cli/browserDefaults.js +2 -1
  19. package/dist/src/cli/docsCheck.js +186 -0
  20. package/dist/src/cli/engine.js +11 -4
  21. package/dist/src/cli/options.js +12 -6
  22. package/dist/src/cli/perfTrace.js +242 -0
  23. package/dist/src/cli/promptRequirement.js +2 -0
  24. package/dist/src/cli/providerDoctor.js +85 -0
  25. package/dist/src/cli/runOptions.js +46 -16
  26. package/dist/src/cli/sessionDisplay.js +39 -4
  27. package/dist/src/cli/sessionLifecycle.js +38 -0
  28. package/dist/src/cli/sessionRunner.js +228 -3
  29. package/dist/src/cli/sessionTable.js +2 -1
  30. package/dist/src/duration.js +47 -0
  31. package/dist/src/mcp/tools/consult.js +19 -3
  32. package/dist/src/mcp/types.js +5 -2
  33. package/dist/src/mcp/utils.js +4 -1
  34. package/dist/src/oracle/baseUrl.js +17 -0
  35. package/dist/src/oracle/client.js +1 -22
  36. package/dist/src/oracle/config.js +17 -4
  37. package/dist/src/oracle/gemini.js +2 -22
  38. package/dist/src/oracle/geminiModels.js +21 -0
  39. package/dist/src/oracle/modelResolver.js +7 -1
  40. package/dist/src/oracle/multiModelRunner.js +20 -2
  41. package/dist/src/oracle/providerFailures.js +204 -0
  42. package/dist/src/oracle/providerRoutePlan.js +281 -0
  43. package/dist/src/oracle/providerRouting.js +92 -0
  44. package/dist/src/oracle/run.js +157 -54
  45. package/dist/src/oracle.js +1 -0
  46. package/dist/src/remote/client.js +8 -0
  47. package/dist/src/remote/server.js +26 -0
  48. package/dist/src/sessionManager.js +5 -1
  49. 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
- ? ' You are in Temporary Chat mode; Pro models are not available there. Remove "temporary-chat=true" from --chatgpt-url or use a non-Pro model (e.g. gpt-5.2).'
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 === "gpt-5.5-pro" ||
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
- const hasProSignal = resolved.includes(" pro") ||
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
- if (!hasProSignal || (resolved.includes("thinking") && !resolved.includes("pro"))) {
55
- throw new Error(`Model picker selected "${resolvedLabel}" while "${desiredModel}" requires GPT-5.5 Pro Extended. Use model "gpt-5.5" with browser thinking time "heavy" for Thinking Heavy.`);
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 (desiredVersion !== '5-5') return false;
160
+ if (!targetUsesCurrentGpt55Alias) return false;
126
161
  const label = normalizeText(value);
127
162
  if (wantsPro) {
128
- return label.includes('pro') && label.includes('extended') && !label.includes('thinking');
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
- return label.includes('thinking') && label.includes('heavy') && !label.includes('pro');
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 && !normalizedLabel.includes(' pro')) return false;
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 === 'pro' ||
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.includes(' pro')) {
448
+ if (!labelHasProWord(normalizedText)) {
406
449
  score -= 80;
407
450
  }
408
- } else if (normalizedText.includes(' pro')) {
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', 'continue with', 'sign up for free'].some(
392
- (needle) => normalized.startsWith(needle),
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
- const labelText = (node) =>
294
- [
295
- node?.textContent,
296
- node?.getAttribute?.('aria-label'),
297
- node?.getAttribute?.('title'),
298
- node?.getAttribute?.('data-testid'),
299
- ]
300
- .filter(Boolean)
301
- .join(' ')
302
- .toLowerCase();
303
- const match = (node, name) => labelText(node).includes(name);
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, document])).filter(Boolean);
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
- attachmentRoots.some((root) =>
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
- const deadline = Date.now() + 20_000;
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.closest('[role="menuitem"], [role="menuitemradio"], [data-radix-collection-item]');
217
- if (row && (optionIsSelected(row) || row.querySelector('[aria-checked="true"]'))) return t;
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 trailings[0];
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 { isTemporaryChatUrl, normalizeChatgptUrl } from "./utils.js";
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;
@@ -1,5 +1,5 @@
1
1
  export const CHATGPT_URL = "https://chatgpt.com/";
2
- export const DEFAULT_MODEL_TARGET = "GPT-5.5 Pro";
2
+ export const DEFAULT_MODEL_TARGET = "Pro";
3
3
  export const DEFAULT_MODEL_STRATEGY = "select";
4
4
  export const COOKIE_URLS = [
5
5
  "https://chatgpt.com",