@steipete/oracle 0.14.0 → 0.14.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.
Files changed (69) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/oracle-cli.js +8 -8
  3. package/dist/src/browser/actions/assistantResponse.js +2 -1
  4. package/dist/src/browser/actions/modelSelection.js +344 -29
  5. package/dist/src/browser/actions/thinkingTime.js +239 -46
  6. package/dist/src/browser/chatgptImages.js +126 -24
  7. package/dist/src/browser/index.js +63 -8
  8. package/dist/src/browser/projectSourcesRunner.js +2 -1
  9. package/dist/src/browser/prompt.js +151 -22
  10. package/dist/src/cli/browserConfig.js +2 -1
  11. package/dist/src/cli/browserDefaults.js +2 -1
  12. package/dist/src/cli/options.js +8 -0
  13. package/dist/src/mcp/tools/chatgptImage.js +8 -3
  14. package/dist/src/mcp/tools/consult.js +9 -8
  15. package/dist/src/mcp/types.js +11 -2
  16. package/dist/src/oracle/thinkingTime.js +40 -0
  17. package/dist/vendor/oracle-notifier/build-notifier.sh +0 -0
  18. package/package.json +32 -34
  19. package/vendor/oracle-notifier/build-notifier.sh +0 -0
  20. package/dist/bin/oracle.js +0 -569
  21. package/dist/docs-site/.nojekyll +0 -0
  22. package/dist/docs-site/CNAME +0 -1
  23. package/dist/docs-site/RELEASING.html +0 -410
  24. package/dist/docs-site/agents.html +0 -374
  25. package/dist/docs-site/anthropic.html +0 -368
  26. package/dist/docs-site/bridge.html +0 -400
  27. package/dist/docs-site/browser-mode.html +0 -593
  28. package/dist/docs-site/chromium-forks.html +0 -347
  29. package/dist/docs-site/cli-reference.html +0 -346
  30. package/dist/docs-site/configuration.html +0 -452
  31. package/dist/docs-site/favicon.svg +0 -14
  32. package/dist/docs-site/followup.html +0 -375
  33. package/dist/docs-site/gemini.html +0 -383
  34. package/dist/docs-site/grok.html +0 -325
  35. package/dist/docs-site/index.html +0 -360
  36. package/dist/docs-site/install.html +0 -335
  37. package/dist/docs-site/linux.html +0 -321
  38. package/dist/docs-site/llms.txt +0 -43
  39. package/dist/docs-site/manual-tests.html +0 -596
  40. package/dist/docs-site/mcp.html +0 -391
  41. package/dist/docs-site/multimodel.html +0 -364
  42. package/dist/docs-site/mythical-pro-agents.html +0 -360
  43. package/dist/docs-site/notifier.html +0 -338
  44. package/dist/docs-site/openai-endpoints.html +0 -387
  45. package/dist/docs-site/openrouter.html +0 -344
  46. package/dist/docs-site/quickstart.html +0 -369
  47. package/dist/docs-site/refactor/ux.html +0 -532
  48. package/dist/docs-site/sessions.html +0 -388
  49. package/dist/docs-site/social-card.png +0 -0
  50. package/dist/docs-site/social-card.svg +0 -79
  51. package/dist/docs-site/spec.html +0 -363
  52. package/dist/docs-site/testing.html +0 -320
  53. package/dist/docs-site/tui-debug.html +0 -326
  54. package/dist/docs-site/windows-work.html +0 -323
  55. package/dist/docs-site/windows.html +0 -320
  56. package/dist/src/browser/chromeCookies.js +0 -312
  57. package/dist/src/browser/keytarShim.js +0 -56
  58. package/dist/src/browser/windowsCookies.js +0 -219
  59. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  60. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  61. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  62. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  63. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  64. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  65. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +0 -20
  66. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  67. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  68. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +0 -128
  69. package/vendor/oracle-notifier/README.md +0 -26
@@ -1,6 +1,10 @@
1
1
  import { 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 BROWSER_THINKING_LOG_PREFIX = "[browser] Thinking time:";
5
+ function formatBrowserThinkingLog(message) {
6
+ return `${BROWSER_THINKING_LOG_PREFIX} ${message.replace(/^Thinking time:\s*/, "")}`;
7
+ }
4
8
  /**
5
9
  * Surfaces the model-picker snapshot captured alongside a failed detection.
6
10
  *
@@ -26,13 +30,14 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
26
30
  const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
27
31
  const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
28
32
  const targetModelKind = inferThinkingTargetModelKind(desiredModel);
29
- const strictProEffort = targetModelKind === "pro" && level === "extended";
33
+ const observedModelKind = result && "modelKind" in result ? result.modelKind : null;
34
+ const strictProEffort = (targetModelKind === "pro" || observedModelKind === "pro") && level === "extended";
30
35
  switch (result?.status) {
31
36
  case "already-selected":
32
- logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
37
+ logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} (already selected)`));
33
38
  return;
34
39
  case "switched":
35
- logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
40
+ logger(formatBrowserThinkingLog(result.label ?? capitalizedLevel));
36
41
  return;
37
42
  case "chip-not-found":
38
43
  case "menu-not-found":
@@ -50,7 +55,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
50
55
  if (strictProEffort) {
51
56
  throw new Error(`${message}; refusing to submit without confirmed Pro Extended.`);
52
57
  }
53
- logger(`${message}; continuing with ChatGPT default.`);
58
+ logger(formatBrowserThinkingLog(`${message}; continuing with ChatGPT default.`));
54
59
  return;
55
60
  }
56
61
  default: {
@@ -59,7 +64,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
59
64
  if (strictProEffort) {
60
65
  throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed Pro Extended.`);
61
66
  }
62
- logger(`Thinking time: unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`);
67
+ logger(formatBrowserThinkingLog(`unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`));
63
68
  return;
64
69
  }
65
70
  }
@@ -75,10 +80,10 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desi
75
80
  const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
76
81
  switch (result?.status) {
77
82
  case "already-selected":
78
- logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
83
+ logger(formatBrowserThinkingLog(`${result.label ?? capitalizedLevel} (already selected)`));
79
84
  return true;
80
85
  case "switched":
81
- logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
86
+ logger(formatBrowserThinkingLog(result.label ?? capitalizedLevel));
82
87
  return true;
83
88
  case "chip-not-found":
84
89
  case "menu-not-found":
@@ -86,12 +91,12 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desi
86
91
  case "selection-unverified":
87
92
  case "model-kind-not-found":
88
93
  if (logger.verbose) {
89
- logger(`Thinking time: ${result.status.replaceAll("-", " ")}; continuing with default.`);
94
+ logger(formatBrowserThinkingLog(`${result.status.replaceAll("-", " ")}; continuing with default.`));
90
95
  }
91
96
  return false;
92
97
  default:
93
98
  if (logger.verbose) {
94
- logger("Thinking time: unknown outcome; continuing with default.");
99
+ logger(formatBrowserThinkingLog("unknown outcome; continuing with default."));
95
100
  }
96
101
  return false;
97
102
  }
@@ -99,7 +104,7 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desi
99
104
  catch (error) {
100
105
  const message = error instanceof Error ? error.message : String(error);
101
106
  if (logger.verbose) {
102
- logger(`Thinking time selection failed (${message}); continuing with default.`);
107
+ logger(formatBrowserThinkingLog(`selection failed (${message}); continuing with default.`));
103
108
  await logDomFailure(Runtime, logger, "thinking-time");
104
109
  }
105
110
  return false;
@@ -130,10 +135,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
130
135
 
131
136
  // Bilingual matchers: English level token + observed Chinese variants.
132
137
  const LEVEL_TOKENS = {
133
- light: ['light', '轻'],
134
- standard: ['standard', '标准'],
135
- extended: ['extended', '扩展', '深度', '加强'],
136
- heavy: ['heavy', '重度', '加重', '高'],
138
+ light: ['light', 'instant', '轻'],
139
+ standard: ['standard', 'medium', '标准'],
140
+ extended: ['extended', 'high', '扩展', '深度', '加强'],
141
+ heavy: ['heavy', 'extra high', '重度', '加重', '高'],
137
142
  };
138
143
  const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
139
144
 
@@ -152,11 +157,36 @@ function buildThinkingTimeExpression(level, desiredModel) {
152
157
  .replace(/[^a-z0-9\\u4e00-\\u9fa5]+/g, ' ')
153
158
  .replace(/\\s+/g, ' ')
154
159
  .trim();
160
+ const hasToken = (text, token) => normalize(text).split(' ').includes(token);
155
161
  const matchesLevel = (text) => {
156
162
  const t = normalize(text);
157
- return targetTokens.some((tok) => t.includes(String(tok).toLowerCase()));
163
+ if (!t) return false;
164
+ return targetTokens.some((tok) => {
165
+ const token = normalize(tok);
166
+ if (!token) return false;
167
+ if (token === 'high') return hasToken(t, 'high') && !hasToken(t, 'extra');
168
+ if (token === 'extra high') return hasToken(t, 'extra') && hasToken(t, 'high');
169
+ return t === token || hasToken(t, token) || t.includes(token);
170
+ });
171
+ };
172
+ const matchesAnyEffortLevel = (text) => {
173
+ const normalizedText = normalize(text);
174
+ if (!normalizedText) return false;
175
+ for (const tokens of Object.values(LEVEL_TOKENS)) {
176
+ for (const rawToken of tokens) {
177
+ const token = normalize(rawToken);
178
+ if (!token) continue;
179
+ if (token.includes(' ')) {
180
+ if (token.split(' ').every((part) => hasToken(normalizedText, part))) return true;
181
+ } else if (/^[a-z0-9]+$/.test(token)) {
182
+ if (hasToken(normalizedText, token)) return true;
183
+ } else if (normalizedText.includes(token)) {
184
+ return true;
185
+ }
186
+ }
187
+ }
188
+ return false;
158
189
  };
159
- const hasToken = (text, token) => normalize(text).split(' ').includes(token);
160
190
  const optionIsSelected = (node) => {
161
191
  if (!(node instanceof HTMLElement)) return false;
162
192
  const ariaChecked = node.getAttribute('aria-checked');
@@ -180,9 +210,28 @@ function buildThinkingTimeExpression(level, desiredModel) {
180
210
  );
181
211
  } catch {}
182
212
  };
213
+ const dispatchHoverSequence = (target) => {
214
+ if (!target || !(target instanceof EventTarget)) return false;
215
+ const types = ['pointerover', 'pointerenter', 'mouseover', 'mouseenter', 'pointermove', 'mousemove'];
216
+ for (const type of types) {
217
+ try {
218
+ const common = { bubbles: true, cancelable: true, view: window };
219
+ const event =
220
+ type.startsWith('pointer') && 'PointerEvent' in window
221
+ ? new PointerEvent(type, { ...common, pointerId: 1, pointerType: 'mouse' })
222
+ : new MouseEvent(type, common);
223
+ target.dispatchEvent(event);
224
+ } catch {}
225
+ }
226
+ try {
227
+ target.focus?.();
228
+ } catch {}
229
+ return true;
230
+ };
183
231
 
184
232
  const TRAILING_SELECTOR = '[data-model-picker-thinking-effort-action="true"]';
185
233
  const INTELLIGENCE_MENU_SELECTOR = '[data-testid="composer-intelligence-picker-content"]';
234
+ const PRO_EFFORT_TRIGGER_SELECTOR = '[data-testid="composer-intelligence-pro-thinking-effort-trigger"]';
186
235
 
187
236
  const findModelButton = () => document.querySelector(MODEL_BUTTON_SELECTOR);
188
237
  const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
@@ -270,13 +319,57 @@ function buildThinkingTimeExpression(level, desiredModel) {
270
319
  return { error: redactDiagnosticText(err && err.message ? err.message : err) };
271
320
  }
272
321
  };
322
+ const modelKindFromNode = (button) => {
323
+ const label = normalize(
324
+ (button?.textContent ?? '') + ' ' + (button?.getAttribute?.('aria-label') ?? ''),
325
+ );
326
+ if (hasToken(label, 'pro')) return 'pro';
327
+ if (hasToken(label, 'thinking')) return 'thinking';
328
+ if (hasToken(label, 'instant')) return 'instant';
329
+ return null;
330
+ };
331
+ const currentModelKind = () => modelKindFromNode(findModelButton());
332
+ const effectiveTargetModelKind = () => TARGET_MODEL_KIND || currentModelKind();
333
+ const isIntelligenceEffortMenu = (menu) => {
334
+ if (menu?.getAttribute?.('data-testid') === 'composer-intelligence-picker-content') {
335
+ return true;
336
+ }
337
+ const label = menu?.querySelector?.('.__menu-label, [class*="menu-label"]');
338
+ return normalize(label?.textContent ?? '').includes('intelligence');
339
+ };
273
340
  const failure = (status, extra = {}) => ({
274
341
  status,
342
+ modelKind: effectiveTargetModelKind(),
275
343
  ...extra,
276
344
  diagnostic: collectPickerDiagnostic(),
277
345
  });
278
- const findOptionInMenu = (menu) => {
279
- for (const item of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
346
+ const findOptionInMenu = (menu, modelKindOverride = null) => {
347
+ const items = Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR));
348
+ const modelKind = modelKindOverride || effectiveTargetModelKind();
349
+ if (modelKind === 'pro') {
350
+ for (const item of items) {
351
+ const itemText = normalize(
352
+ (item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
353
+ );
354
+ if (
355
+ hasToken(itemText, 'pro') &&
356
+ (matchesLevel(item.textContent ?? '') ||
357
+ matchesLevel(item.getAttribute?.('aria-label') ?? ''))
358
+ ) {
359
+ return item;
360
+ }
361
+ }
362
+ if (isIntelligenceEffortMenu(menu)) {
363
+ return null;
364
+ }
365
+ }
366
+ for (const item of items) {
367
+ const itemText = normalize(
368
+ (item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
369
+ );
370
+ if (modelKind && modelKind !== 'pro' && hasToken(itemText, 'pro')) {
371
+ continue;
372
+ }
280
373
  if (
281
374
  matchesLevel(item.textContent ?? '') ||
282
375
  matchesLevel(item.getAttribute?.('aria-label') ?? '')
@@ -296,14 +389,21 @@ function buildThinkingTimeExpression(level, desiredModel) {
296
389
  };
297
390
  const isEffortMenu = (menu) => {
298
391
  if (!isVisible(menu)) return false;
392
+ if (menu.getAttribute?.('data-testid') === 'composer-intelligence-picker-content') return true;
299
393
  const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
300
394
  const labelText = normalize(label?.textContent ?? '');
301
395
  return (
396
+ labelText.includes('intelligence') ||
302
397
  labelText.includes('thinking time') ||
303
398
  labelText.includes('thinking effort') ||
304
399
  countEffortLevels(menu) >= 2
305
400
  );
306
401
  };
402
+ const isProEffortMenu = (menu) => {
403
+ if (!isVisible(menu)) return false;
404
+ const text = normalize(menu?.textContent ?? '');
405
+ return text.includes('pro standard') && text.includes('pro extended');
406
+ };
307
407
  const controlledMenu = (trigger) => {
308
408
  const id = trigger?.getAttribute?.('aria-controls');
309
409
  if (!id) return null;
@@ -318,9 +418,69 @@ function buildThinkingTimeExpression(level, desiredModel) {
318
418
  }
319
419
  return null;
320
420
  };
321
- const selectAndVerify = async (trigger, findOption) => {
421
+ const controlledProEffortMenu = (trigger) => {
422
+ const id = trigger?.getAttribute?.('aria-controls');
423
+ if (!id) return null;
424
+ const menu = document.getElementById?.(id);
425
+ return isProEffortMenu(menu) ? menu : null;
426
+ };
427
+ const findVisibleProEffortMenu = (trigger) => {
428
+ const controlled = controlledProEffortMenu(trigger);
429
+ if (controlled) return controlled;
430
+ for (const menu of document.querySelectorAll(MENU_CONTAINER_SELECTOR)) {
431
+ if (isProEffortMenu(menu)) return menu;
432
+ }
433
+ return null;
434
+ };
435
+ const matchesProEffortLevel = (node) => {
436
+ const text = normalize(
437
+ (node?.textContent ?? '') + ' ' + (node?.getAttribute?.('aria-label') ?? ''),
438
+ );
439
+ if (TARGET_LEVEL === 'standard') {
440
+ return text.includes('pro') && text.includes('standard');
441
+ }
442
+ if (TARGET_LEVEL === 'extended') {
443
+ return text.includes('pro') && text.includes('extended');
444
+ }
445
+ return false;
446
+ };
447
+ const findProEffortOptionInMenu = (menu) => {
448
+ for (const item of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
449
+ if (matchesProEffortLevel(item)) return item;
450
+ }
451
+ return null;
452
+ };
453
+ const currentProEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
454
+ const button = trigger?.matches?.('button.__composer-pill') ? trigger : findModelButton();
455
+ if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) !== 'pro') {
456
+ return false;
457
+ }
458
+ const label = normalize(button?.textContent ?? '');
459
+ if (TARGET_LEVEL === 'standard') {
460
+ return hasToken(label, 'pro') && !hasToken(label, 'extended');
461
+ }
462
+ if (TARGET_LEVEL === 'extended') {
463
+ return hasToken(label, 'pro') && hasToken(label, 'extended');
464
+ }
465
+ return false;
466
+ };
467
+ const currentEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
468
+ if (currentProEffortPillMatchesTarget(trigger, modelKindOverride)) return true;
469
+ const button = trigger?.matches?.('button.__composer-pill') ? trigger : findModelButton();
470
+ if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) === 'pro') {
471
+ return false;
472
+ }
473
+ const label = (button?.textContent ?? '') + ' ' + (button?.getAttribute?.('aria-label') ?? '');
474
+ return matchesLevel(label);
475
+ };
476
+ const selectAndVerify = async (trigger, findOption, modelKindOverride = null) => {
322
477
  const option = findOption();
323
- if (!option) return failure('option-not-found');
478
+ const triggerModelKind =
479
+ modelKindOverride ||
480
+ TARGET_MODEL_KIND ||
481
+ modelKindFromNode(trigger) ||
482
+ effectiveTargetModelKind();
483
+ if (!option) return failure('option-not-found', { modelKind: triggerModelKind });
324
484
  const label = option.textContent?.trim?.() || null;
325
485
  if (optionIsSelected(option)) {
326
486
  closeOpenMenus();
@@ -334,6 +494,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
334
494
  closeOpenMenus();
335
495
  return { status: 'switched', label: refreshed.textContent?.trim?.() || label };
336
496
  }
497
+ if (currentEffortPillMatchesTarget(trigger, triggerModelKind)) {
498
+ closeOpenMenus();
499
+ return { status: 'switched', label };
500
+ }
337
501
 
338
502
  if (!refreshed && trigger && trigger.getAttribute?.('aria-expanded') !== 'true') {
339
503
  dispatchClickSequence(trigger);
@@ -346,29 +510,56 @@ function buildThinkingTimeExpression(level, desiredModel) {
346
510
  closeOpenMenus();
347
511
  return { status: 'switched', label: selected.textContent?.trim?.() || label };
348
512
  }
513
+ if (currentEffortPillMatchesTarget(trigger, triggerModelKind)) {
514
+ closeOpenMenus();
515
+ return { status: 'switched', label };
516
+ }
349
517
  await sleep(100);
350
518
  }
351
- const result = failure('selection-unverified');
519
+ const result = failure('selection-unverified', { modelKind: triggerModelKind });
352
520
  closeOpenMenus();
353
521
  return result;
354
522
  };
523
+ const selectProEffortFromSubmenu = async () => {
524
+ if (TARGET_MODEL_KIND !== 'pro' || (TARGET_LEVEL !== 'standard' && TARGET_LEVEL !== 'extended')) {
525
+ return null;
526
+ }
527
+ const trigger = document.querySelector(PRO_EFFORT_TRIGGER_SELECTOR);
528
+ if (!trigger) {
529
+ return null;
530
+ }
531
+ dispatchHoverSequence(trigger);
532
+ if (trigger.getAttribute?.('aria-expanded') !== 'true') {
533
+ dispatchClickSequence(trigger);
534
+ }
535
+ const deadline = performance.now() + MAX_WAIT_MS;
536
+ while (performance.now() < deadline) {
537
+ const menu = findVisibleProEffortMenu(trigger);
538
+ if (menu) {
539
+ return selectAndVerify(trigger, () => {
540
+ const currentMenu = findVisibleProEffortMenu(trigger);
541
+ return currentMenu ? findProEffortOptionInMenu(currentMenu) : null;
542
+ });
543
+ }
544
+ await sleep(100);
545
+ }
546
+ return null;
547
+ };
355
548
 
356
549
  // Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
357
550
  // controlled menu contains the effort levels. Prefer this ownership boundary
358
551
  // before probing older model-picker layouts.
359
552
  const COMPOSER_EFFORT_PILL_SELECTORS = [
360
- 'form button.__composer-pill[aria-haspopup="menu"]',
361
- '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
362
- '.__composer-pill-composite button[aria-haspopup="menu"]',
553
+ 'form button.__composer-pill',
554
+ '[data-testid="composer-footer-actions"] button.__composer-pill',
555
+ '.__composer-pill-composite button.__composer-pill',
363
556
  ];
364
557
  const findComposerEffortPill = () => {
365
- const candidates = [];
366
558
  const seen = new Set();
367
559
  for (const selector of COMPOSER_EFFORT_PILL_SELECTORS) {
368
560
  for (const button of document.querySelectorAll(selector)) {
369
561
  if (seen.has(button) || !isVisible(button)) continue;
370
562
  seen.add(button);
371
- if (button.getAttribute?.('aria-haspopup') !== 'menu') continue;
372
563
  if (button.getAttribute?.('data-testid') === 'model-switcher-dropdown-button') continue;
373
564
  const label = normalize(
374
565
  (button.getAttribute?.('aria-label') ?? '') + ' ' +
@@ -378,21 +569,19 @@ function buildThinkingTimeExpression(level, desiredModel) {
378
569
  if (
379
570
  (TARGET_MODEL_KIND === 'pro' && hasToken(label, 'pro') && !hasToken(label, 'thinking')) ||
380
571
  (TARGET_MODEL_KIND === 'thinking' && hasToken(label, 'thinking') && !hasToken(label, 'pro')) ||
381
- (!TARGET_MODEL_KIND && hasToken(label, 'thinking'))
572
+ (!TARGET_MODEL_KIND && hasToken(label, 'thinking')) ||
573
+ (button.matches?.('button.__composer-pill') && matchesAnyEffortLevel(label))
382
574
  ) {
383
575
  return button;
384
576
  }
385
- candidates.push(button);
386
577
  }
387
578
  }
388
- const composerPills = candidates.filter((button) =>
389
- button.matches?.('button.__composer-pill'),
390
- );
391
- return composerPills.length === 1 ? composerPills[0] : null;
579
+ return null;
392
580
  };
393
581
 
394
582
  const composerEffortPill = findComposerEffortPill();
395
583
  if (composerEffortPill) {
584
+ const composerModelKind = TARGET_MODEL_KIND || modelKindFromNode(composerEffortPill);
396
585
  if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
397
586
  dispatchClickSequence(composerEffortPill);
398
587
  await sleep(INITIAL_WAIT_MS);
@@ -401,14 +590,24 @@ function buildThinkingTimeExpression(level, desiredModel) {
401
590
  while (performance.now() < deadline) {
402
591
  const menu = findVisibleEffortMenu(composerEffortPill);
403
592
  if (menu) {
404
- return selectAndVerify(composerEffortPill, () => {
405
- const currentMenu = findVisibleEffortMenu(composerEffortPill);
406
- return currentMenu ? findOptionInMenu(currentMenu) : null;
407
- });
593
+ const proEffortResult = await selectProEffortFromSubmenu();
594
+ if (proEffortResult) {
595
+ return proEffortResult;
596
+ }
597
+ return selectAndVerify(
598
+ composerEffortPill,
599
+ () => {
600
+ const currentMenu = findVisibleEffortMenu(composerEffortPill);
601
+ return currentMenu ? findOptionInMenu(currentMenu, composerModelKind) : null;
602
+ },
603
+ composerModelKind,
604
+ );
408
605
  }
409
606
  await sleep(100);
410
607
  }
411
- const result = failure('menu-not-found');
608
+ const result = failure('menu-not-found', {
609
+ modelKind: composerModelKind,
610
+ });
412
611
  closeOpenMenus();
413
612
  return result;
414
613
  }
@@ -512,15 +711,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
512
711
  // ---------- COMPATIBILITY UI: unified "Intelligence" effort picker ----------
513
712
  // One observed ChatGPT layout replaced the per-model trailing buttons with a single
514
713
  // "Intelligence" menu ([data-testid="composer-intelligence-picker-content"]),
515
- // whose role="menuitemradio" rows are the effort tiers. For the Pro model the
516
- // combined "Pro Extended" row carries aria-checked when active, and Pro
517
- // sub-options live behind
518
- // [data-testid="composer-intelligence-pro-thinking-effort-trigger"]. We
519
- // confirm Pro Extended by the radio's checked state (real proof), never by
520
- // the composer-pill label. The new non-pro tiers (Instant/Medium/High/Extra
521
- // High) don't map cleanly onto light/standard/extended/heavy, so we only
522
- // drive this picker for the strict Pro Extended target and let other levels
523
- // fall through to the legacy paths below.
714
+ // whose role="menuitemradio" rows are the effort tiers. We verify the checked
715
+ // radio instead of trusting the composer-pill label; non-Pro targets also
716
+ // explicitly skip Pro rows before matching effort labels.
524
717
  if (TARGET_MODEL_KIND === 'pro' && TARGET_LEVEL === 'extended') {
525
718
  const matchesProExtended = (node) => {
526
719
  const text = normalize(