@steipete/oracle 0.14.0 → 0.15.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 (31) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/oracle-cli.js +10 -8
  3. package/dist/docs-site/browser-mode.html +3 -2
  4. package/dist/docs-site/cli-reference.html +1 -1
  5. package/dist/docs-site/mcp.html +1 -1
  6. package/dist/src/browser/actions/assistantResponse.js +2 -1
  7. package/dist/src/browser/actions/deepResearch.js +132 -61
  8. package/dist/src/browser/actions/modelSelection.js +388 -30
  9. package/dist/src/browser/actions/thinkingTime.js +303 -65
  10. package/dist/src/browser/artifacts.js +2 -8
  11. package/dist/src/browser/chatgptFiles.js +198 -49
  12. package/dist/src/browser/chatgptImages.js +126 -24
  13. package/dist/src/browser/chromeLifecycle.js +35 -4
  14. package/dist/src/browser/deepResearchResult.js +23 -0
  15. package/dist/src/browser/index.js +145 -19
  16. package/dist/src/browser/profileCopy.js +93 -0
  17. package/dist/src/browser/projectSourcesRunner.js +2 -1
  18. package/dist/src/browser/prompt.js +151 -22
  19. package/dist/src/cli/browserConfig.js +19 -2
  20. package/dist/src/cli/browserDefaults.js +2 -1
  21. package/dist/src/cli/options.js +8 -0
  22. package/dist/src/cli/sessionRunner.js +13 -7
  23. package/dist/src/mcp/tools/chatgptImage.js +8 -3
  24. package/dist/src/mcp/tools/consult.js +9 -8
  25. package/dist/src/mcp/types.js +11 -2
  26. package/dist/src/oracle/thinkingTime.js +40 -0
  27. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  28. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  29. package/package.json +6 -6
  30. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  31. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
@@ -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,72 @@ 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'),
579
+ return null;
580
+ };
581
+ let composerEffortPill = findComposerEffortPill();
582
+ let modelBtn = findModelButton();
583
+ const modelKindFromLegacyTrailing = (trailing) => {
584
+ const row = trailing.closest?.(
585
+ '[role="menuitem"], [role="menuitemradio"], [data-radix-collection-item]',
586
+ );
587
+ const idText = normalize(
588
+ (row?.getAttribute?.('data-testid') ?? '') + ' ' +
589
+ (trailing.getAttribute?.('data-testid') ?? '')
390
590
  );
391
- return composerPills.length === 1 ? composerPills[0] : null;
591
+ if (!idText.includes('model switcher')) return null;
592
+ const modelPart = normalize(idText.replace(/\\bthinking effort\\b.*$/, ''));
593
+ if (hasToken(modelPart, 'pro')) return 'pro';
594
+ if (hasToken(modelPart, 'thinking')) return 'thinking';
595
+ if (hasToken(modelPart, 'instant')) return 'instant';
596
+ return null;
392
597
  };
393
-
394
- const composerEffortPill = findComposerEffortPill();
598
+ const legacyEffortOwnerIsReady = () => {
599
+ if (
600
+ TARGET_MODEL_KIND === 'pro' &&
601
+ TARGET_LEVEL === 'extended' &&
602
+ isVisible(document.querySelector(INTELLIGENCE_MENU_SELECTOR))
603
+ ) {
604
+ return true;
605
+ }
606
+ const expectedKind = TARGET_MODEL_KIND || modelKindFromNode(modelBtn);
607
+ return Boolean(
608
+ expectedKind &&
609
+ findTrailingButtons().some(
610
+ (button) => isVisible(button) && modelKindFromLegacyTrailing(button) === expectedKind,
611
+ ),
612
+ );
613
+ };
614
+ let attemptedModelButton =
615
+ modelBtn?.getAttribute?.('aria-expanded') === 'true' ? modelBtn : null;
616
+ const effortOwnerDeadline = performance.now() + MAX_WAIT_MS;
617
+ while (!composerEffortPill && performance.now() < effortOwnerDeadline) {
618
+ if (
619
+ modelBtn &&
620
+ attemptedModelButton !== modelBtn &&
621
+ modelBtn.getAttribute?.('aria-expanded') !== 'true'
622
+ ) {
623
+ dispatchClickSequence(modelBtn);
624
+ attemptedModelButton = modelBtn;
625
+ await sleep(INITIAL_WAIT_MS);
626
+ }
627
+ if (modelBtn && legacyEffortOwnerIsReady()) break;
628
+ await sleep(100);
629
+ composerEffortPill = findComposerEffortPill();
630
+ modelBtn = findModelButton();
631
+ if (modelBtn?.getAttribute?.('aria-expanded') === 'true') {
632
+ attemptedModelButton = modelBtn;
633
+ }
634
+ }
395
635
  if (composerEffortPill) {
636
+ if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
637
+ const composerModelKind = TARGET_MODEL_KIND || modelKindFromNode(composerEffortPill);
396
638
  if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
397
639
  dispatchClickSequence(composerEffortPill);
398
640
  await sleep(INITIAL_WAIT_MS);
@@ -401,14 +643,24 @@ function buildThinkingTimeExpression(level, desiredModel) {
401
643
  while (performance.now() < deadline) {
402
644
  const menu = findVisibleEffortMenu(composerEffortPill);
403
645
  if (menu) {
404
- return selectAndVerify(composerEffortPill, () => {
405
- const currentMenu = findVisibleEffortMenu(composerEffortPill);
406
- return currentMenu ? findOptionInMenu(currentMenu) : null;
407
- });
646
+ const proEffortResult = await selectProEffortFromSubmenu();
647
+ if (proEffortResult) {
648
+ return proEffortResult;
649
+ }
650
+ return selectAndVerify(
651
+ composerEffortPill,
652
+ () => {
653
+ const currentMenu = findVisibleEffortMenu(composerEffortPill);
654
+ return currentMenu ? findOptionInMenu(currentMenu, composerModelKind) : null;
655
+ },
656
+ composerModelKind,
657
+ );
408
658
  }
409
659
  await sleep(100);
410
660
  }
411
- const result = failure('menu-not-found');
661
+ const result = failure('menu-not-found', {
662
+ modelKind: composerModelKind,
663
+ });
412
664
  closeOpenMenus();
413
665
  return result;
414
666
  }
@@ -447,22 +699,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
447
699
  (trailing.getAttribute?.('data-testid') ?? '')
448
700
  );
449
701
  };
450
- const testIdTextForTrailing = (trailing) => {
451
- const row = rowForTrailing(trailing) || findEffortRow(trailing);
452
- return normalize(
453
- (row?.getAttribute?.('data-testid') ?? '') + ' ' +
454
- (trailing.getAttribute?.('data-testid') ?? '')
455
- );
456
- };
457
- const modelKindFromTrailing = (trailing) => {
458
- const idText = testIdTextForTrailing(trailing);
459
- if (!idText.includes('model switcher')) return null;
460
- const modelPart = normalize(idText.replace(/\\bthinking effort\\b.*$/, ''));
461
- if (hasToken(modelPart, 'pro')) return 'pro';
462
- if (hasToken(modelPart, 'thinking')) return 'thinking';
463
- if (hasToken(modelPart, 'instant')) return 'instant';
464
- return null;
465
- };
702
+ const modelKindFromTrailing = modelKindFromLegacyTrailing;
466
703
  const trailingMatchesTargetModelKind = (trailing) => {
467
704
  if (!TARGET_MODEL_KIND) return false;
468
705
  const idKind = modelKindFromTrailing(trailing);
@@ -499,12 +736,19 @@ function buildThinkingTimeExpression(level, desiredModel) {
499
736
  return null;
500
737
  };
501
738
 
502
- const modelBtn = findModelButton();
739
+ const modelButtonDeadline = performance.now() + MAX_WAIT_MS;
740
+ while (!modelBtn && performance.now() < modelButtonDeadline) {
741
+ await sleep(100);
742
+ modelBtn = findModelButton();
743
+ }
503
744
  if (!modelBtn) {
504
745
  return failure('chip-not-found');
505
746
  }
506
747
  // Open model menu (idempotent — leaves it open if already open).
507
- if (modelBtn.getAttribute('aria-expanded') !== 'true') {
748
+ if (
749
+ modelBtn.getAttribute('aria-expanded') !== 'true' &&
750
+ !legacyEffortOwnerIsReady()
751
+ ) {
508
752
  dispatchClickSequence(modelBtn);
509
753
  await sleep(INITIAL_WAIT_MS);
510
754
  }
@@ -512,15 +756,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
512
756
  // ---------- COMPATIBILITY UI: unified "Intelligence" effort picker ----------
513
757
  // One observed ChatGPT layout replaced the per-model trailing buttons with a single
514
758
  // "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.
759
+ // whose role="menuitemradio" rows are the effort tiers. We verify the checked
760
+ // radio instead of trusting the composer-pill label; non-Pro targets also
761
+ // explicitly skip Pro rows before matching effort labels.
524
762
  if (TARGET_MODEL_KIND === 'pro' && TARGET_LEVEL === 'extended') {
525
763
  const matchesProExtended = (node) => {
526
764
  const text = normalize(
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { getOracleHomeDir } from "../oracleHome.js";
4
+ import { isDeepResearchIncompleteText } from "./deepResearchResult.js";
4
5
  const ARTIFACTS_DIRNAME = "artifacts";
5
6
  function sanitizePathSegment(value, fallback) {
6
7
  const sanitized = value
@@ -85,16 +86,9 @@ export async function writeBinaryBrowserArtifact(params) {
85
86
  sourceUrl: params.sourceUrl,
86
87
  };
87
88
  }
88
- function isToolOnlyPlaceholder(text) {
89
- const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
90
- return (normalized === "called tool" ||
91
- normalized === "used tool" ||
92
- normalized === "użyto narzędzia" ||
93
- normalized === "narzędzie wywołane");
94
- }
95
89
  export async function saveDeepResearchReportArtifact(params) {
96
90
  const report = params.reportMarkdown.trim();
97
- if (report.length < 40 || isToolOnlyPlaceholder(report)) {
91
+ if (report.length < 40 || isDeepResearchIncompleteText(report)) {
98
92
  return null;
99
93
  }
100
94
  return writeTextBrowserArtifact({