@steipete/oracle 0.15.2 → 0.16.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/oracle-cli.js +1 -1
  3. package/dist/bin/oracle-mcp.js +0 -0
  4. package/dist/src/browser/actions/assistantResponse.js +218 -126
  5. package/dist/src/browser/actions/modelSelection.js +133 -22
  6. package/dist/src/browser/actions/navigation.js +235 -14
  7. package/dist/src/browser/actions/thinkingStatus.js +228 -0
  8. package/dist/src/browser/actions/thinkingTime.js +68 -11
  9. package/dist/src/browser/cdpLiveness.js +78 -0
  10. package/dist/src/browser/chromeLifecycle.js +112 -30
  11. package/dist/src/browser/config.js +14 -1
  12. package/dist/src/browser/constants.js +5 -1
  13. package/dist/src/browser/controlPlan.js +2 -2
  14. package/dist/src/browser/conversationUrl.js +16 -0
  15. package/dist/src/browser/conversationUrlMonitor.js +2 -4
  16. package/dist/src/browser/cookies.js +6 -4
  17. package/dist/src/browser/index.js +213 -57
  18. package/dist/src/browser/liveTabs.js +115 -33
  19. package/dist/src/browser/pageActions.js +1 -1
  20. package/dist/src/browser/projectSourcesRunner.js +4 -4
  21. package/dist/src/browser/reattach.js +2 -2
  22. package/dist/src/browser/reattachHelpers.js +3 -5
  23. package/dist/src/browser/reattachability.js +2 -1
  24. package/dist/src/browser/recoverConversation.js +90 -29
  25. package/dist/src/browser/tabLeaseRegistry.js +9 -3
  26. package/dist/src/cli/browserConfig.js +5 -1
  27. package/dist/src/cli/browserDefaults.js +4 -1
  28. package/dist/src/cli/browserTabs.js +54 -33
  29. package/dist/src/cli/options.js +24 -0
  30. package/dist/src/cli/runOptions.js +6 -1
  31. package/dist/src/cli/sessionDisplay.js +38 -14
  32. package/dist/src/cli/sessionRunner.js +49 -2
  33. package/dist/src/oracle/config.js +25 -0
  34. package/dist/src/oracle/geminiModels.js +2 -0
  35. package/dist/src/remote/server.js +8 -0
  36. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  37. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  38. package/package.json +13 -14
  39. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  40. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  41. package/dist/bin/oracle.js +0 -569
  42. package/dist/docs-site/.nojekyll +0 -0
  43. package/dist/docs-site/CNAME +0 -1
  44. package/dist/docs-site/RELEASING.html +0 -410
  45. package/dist/docs-site/agents.html +0 -374
  46. package/dist/docs-site/anthropic.html +0 -368
  47. package/dist/docs-site/bridge.html +0 -416
  48. package/dist/docs-site/browser-mode.html +0 -594
  49. package/dist/docs-site/chromium-forks.html +0 -347
  50. package/dist/docs-site/cli-reference.html +0 -346
  51. package/dist/docs-site/configuration.html +0 -462
  52. package/dist/docs-site/favicon.svg +0 -14
  53. package/dist/docs-site/followup.html +0 -375
  54. package/dist/docs-site/gemini.html +0 -383
  55. package/dist/docs-site/grok.html +0 -325
  56. package/dist/docs-site/index.html +0 -360
  57. package/dist/docs-site/install.html +0 -335
  58. package/dist/docs-site/linux.html +0 -321
  59. package/dist/docs-site/llms.txt +0 -43
  60. package/dist/docs-site/manual-tests.html +0 -596
  61. package/dist/docs-site/mcp.html +0 -391
  62. package/dist/docs-site/multimodel.html +0 -364
  63. package/dist/docs-site/mythical-pro-agents.html +0 -360
  64. package/dist/docs-site/notifier.html +0 -338
  65. package/dist/docs-site/openai-endpoints.html +0 -399
  66. package/dist/docs-site/openrouter.html +0 -344
  67. package/dist/docs-site/quickstart.html +0 -369
  68. package/dist/docs-site/refactor/ux.html +0 -532
  69. package/dist/docs-site/sessions.html +0 -388
  70. package/dist/docs-site/social-card.png +0 -0
  71. package/dist/docs-site/social-card.svg +0 -79
  72. package/dist/docs-site/spec.html +0 -363
  73. package/dist/docs-site/testing.html +0 -320
  74. package/dist/docs-site/tui-debug.html +0 -326
  75. package/dist/docs-site/windows-work.html +0 -323
  76. package/dist/docs-site/windows.html +0 -320
  77. package/dist/src/browser/chromeCookies.js +0 -312
  78. package/dist/src/browser/keytarShim.js +0 -56
  79. package/dist/src/browser/windowsCookies.js +0 -219
@@ -404,6 +404,234 @@ function buildThinkingStatusExpression() {
404
404
  return null;
405
405
  })()`;
406
406
  }
407
+ // Present-tense/gerund status labels that mean the model is ACTIVELY working. The
408
+ // past-tense "thought for Xs" summary is deliberately excluded: it persists in the DOM
409
+ // on every completed reasoning turn (and on reattach), so treating its mere presence as
410
+ // "still thinking" would veto completion forever and hang the call. Kept in sync with
411
+ // THINKING_STATUS_LABELS in assistantResponse.ts (the connector phases "searching the
412
+ // web"/"reading"/"finalizing answer" are exactly the GPT-5.5 Pro gaps that produce the
413
+ // preamble->answer window this predicate must cover).
414
+ const ACTIVE_THINKING_LABELS = [
415
+ "thinking",
416
+ "pro thinking",
417
+ "thinking longer for a better answer",
418
+ "reasoning",
419
+ "finalizing answer",
420
+ "finalizing",
421
+ "analyzing",
422
+ "researching",
423
+ "working on it",
424
+ "working",
425
+ "planning",
426
+ "searching the web",
427
+ "searching",
428
+ "reading",
429
+ ];
430
+ // buildThinkingActivePredicateJs: a SIDE-EFFECT-FREE injected predicate `${fnName}()` that
431
+ // returns true iff the model is ACTIVELY generating/thinking right now. Unlike
432
+ // buildThinkingStatusExpression it never clicks a disclosure and never keys on the mere
433
+ // PRESENCE of a reasoning container ([data-testid*="reasoning"] persists after completion);
434
+ // it keys only on ACTIVITY signals: a visible stop/interrupt control, a visible animated
435
+ // loading-shimmer skeleton, aria-busy, a visible thinking sidecar panel with live progress,
436
+ // or a visible ACTIVE (present-tense) thinking status label. Used as a completion VETO so a
437
+ // settled preamble is never finalized while the reasoning/tool phase is still running.
438
+ function buildThinkingActivityPredicateJs(fnName, detailed) {
439
+ const stopLiteral = JSON.stringify(STOP_BUTTON_SELECTORS.join(", "));
440
+ const activeLabelsLiteral = JSON.stringify(ACTIVE_THINKING_LABELS);
441
+ const conversationLiteral = JSON.stringify(CONVERSATION_TURN_SELECTOR);
442
+ const assistantLiteral = JSON.stringify(ASSISTANT_ROLE_SELECTOR);
443
+ const strong = detailed ? "{ active: true, strong: true }" : "true";
444
+ const weak = detailed ? "{ active: true, strong: false }" : "true";
445
+ const idle = detailed ? "{ active: false, strong: false }" : "false";
446
+ return `const ${fnName} = () => {
447
+ const STOP_SELECTOR = ${stopLiteral};
448
+ const ACTIVE_LABELS = ${activeLabelsLiteral};
449
+ const CONVERSATION_SELECTOR = ${conversationLiteral};
450
+ const ASSISTANT_SELECTOR = ${assistantLiteral};
451
+ const isVisible = (node) => {
452
+ if (!(node instanceof HTMLElement)) return false;
453
+ const rect = node.getBoundingClientRect();
454
+ if (!rect || rect.width <= 0 || rect.height <= 0) return false;
455
+ const style = window.getComputedStyle(node);
456
+ if (
457
+ style.display === 'none' ||
458
+ style.visibility === 'hidden' ||
459
+ (style.opacity !== '' && Number(style.opacity) === 0)
460
+ ) {
461
+ return false;
462
+ }
463
+ return true;
464
+ };
465
+ const some = (selector, pred) => {
466
+ let nodes;
467
+ try { nodes = document.querySelectorAll(selector); } catch { return false; }
468
+ return Array.from(nodes).some((node) => pred(node));
469
+ };
470
+ // 1) Stop/interrupt control visible -> generation is active (language-independent).
471
+ if (some(STOP_SELECTOR, isVisible)) return ${strong};
472
+ // 2-3) Shimmer/aria-busy are checked only inside the current turn or verified thinking
473
+ // panel below. Page-global busy UI (history/sidebar/upload) is not model activity.
474
+ const hasBusyIndicator = (scope) => {
475
+ let nodes;
476
+ try {
477
+ nodes = scope.querySelectorAll(
478
+ 'span.loading-shimmer, .loading-shimmer, [class*="loading-shimmer"], [aria-busy="true"]',
479
+ );
480
+ } catch { return false; }
481
+ return Array.from(nodes).some((node) => isVisible(node));
482
+ };
483
+ // 4) Active (present-tense) thinking status label near a status/reasoning node.
484
+ const norm = (value) =>
485
+ String(value || '')
486
+ .normalize('NFD')
487
+ .replace(/[\\u0300-\\u036f]/g, '')
488
+ .toLowerCase()
489
+ .replace(/\\s+/g, ' ')
490
+ .trim();
491
+ // Completed reasoning summary: the whole visible label must be a duration summary. Anchoring
492
+ // prevents an early live trace such as "Thought for 2s: Searching the web" from being
493
+ // mistaken for completion before the trace grows beyond an arbitrary length threshold.
494
+ const isCompletedSummary = (text) =>
495
+ /^(?:(?:reasoning|pro thinking)\\s*)?thought for (?:\\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours)(?:\\s+\\d+(?:\\.\\d+)?\\s*(?:s|sec|secs|second|seconds|m|min|mins|minute|minutes|h|hr|hrs|hour|hours))*|(?:a|an) [a-z]+(?: [a-z]+){0,2})(?: edit)?$/.test(text);
496
+ const isActiveLabel = (raw) => {
497
+ const text = norm(raw);
498
+ if (!text || text.length > 60) return false;
499
+ if (isCompletedSummary(text)) return false;
500
+ return ACTIVE_LABELS.some((label) => text === label || text.startsWith(label + ' '));
501
+ };
502
+ const statusNodes = (() => {
503
+ try {
504
+ return Array.from(
505
+ document.querySelectorAll(
506
+ '[data-testid*="thinking"], [data-testid*="reasoning"]',
507
+ ),
508
+ );
509
+ } catch {
510
+ return [];
511
+ }
512
+ })();
513
+ for (const node of statusNodes) {
514
+ if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
515
+ const testId = norm(node.getAttribute('data-testid'));
516
+ const verifiedThinkingChrome = testId.includes('thinking') || testId.includes('reasoning');
517
+ const matches =
518
+ verifiedThinkingChrome &&
519
+ (isActiveLabel(node.textContent) || isActiveLabel(node.getAttribute('aria-label')));
520
+ if (matches) return ${strong};
521
+ }
522
+ // 5) A live progress bar (determinate or indeterminate) is active generation, even when no
523
+ // label or shimmer is present (some connector/tool phases surface only a progress bar).
524
+ const hasLiveProgress = (scope) => {
525
+ let nodes;
526
+ // Only genuine progress indicators — NOT generic [aria-valuenow] range widgets (sliders,
527
+ // spinbuttons), which are not liveness signals and would falsely veto completion forever.
528
+ try {
529
+ nodes = scope.querySelectorAll('progress, [role="progressbar"]');
530
+ } catch { return false; }
531
+ return Array.from(nodes).some((n) => {
532
+ if (!(n instanceof HTMLElement) || !isVisible(n)) return false;
533
+ if (n instanceof HTMLProgressElement) {
534
+ // Determinate <progress> is active only while it has not reached its max.
535
+ if (Number.isFinite(n.value) && Number.isFinite(n.max) && n.max > 0) return n.value < n.max;
536
+ return true; // indeterminate
537
+ }
538
+ const rawNow = n.getAttribute('aria-valuenow');
539
+ if (rawNow != null) {
540
+ const now = Number(rawNow);
541
+ const rawMax = n.getAttribute('aria-valuemax');
542
+ const max = rawMax != null && Number.isFinite(Number(rawMax)) ? Number(rawMax) : 100;
543
+ return Number.isFinite(now) ? now < max : true;
544
+ }
545
+ return true; // role=progressbar with no value -> indeterminate -> active
546
+ });
547
+ };
548
+ // Scoped to the CURRENT assistant turn, not the whole document: unrelated page UI can keep
549
+ // a visible progress bar mounted indefinitely (review P1), and a document-wide veto would
550
+ // then hold thinkingActive true until the watchdog timeout on a completed response. The
551
+ // sidecar check below covers verified reasoning panels; here only the latest turn counts.
552
+ const turns = (() => {
553
+ try { return document.querySelectorAll(CONVERSATION_SELECTOR); } catch { return []; }
554
+ })();
555
+ const lastTurn = turns.length ? turns[turns.length - 1] : null;
556
+ if (
557
+ lastTurn instanceof HTMLElement &&
558
+ (hasBusyIndicator(lastTurn) || hasLiveProgress(lastTurn))
559
+ ) return ${strong};
560
+ // 6) A visible thinking/reasoning sidecar panel (the connector/reasoning phase is often
561
+ // exposed ONLY through a right-side panel with no inline label). Match the existing
562
+ // thinking-monitor heuristic: a right-side panel that looks like thinking, or any such
563
+ // container that carries a live progress bar. Presence alone is NOT enough (a collapsed
564
+ // reasoning summary persists post-completion); require the thinking cue or live progress.
565
+ const looksLikeThinking = (node) => {
566
+ // Judge completion on the panel's VISIBLE text only: data-testid values like
567
+ // "reasoning-panel" would otherwise taint the completed-summary check, and a live trace
568
+ // is judged by its full rendered length, not by fragments of it.
569
+ const visible = norm(node.textContent) || norm(node.getAttribute?.('aria-label'));
570
+ if (isCompletedSummary(visible)) return false;
571
+ if (visible.includes('thought for ')) return true;
572
+ const label = norm([
573
+ node.textContent,
574
+ node.getAttribute?.('aria-label'),
575
+ node.getAttribute?.('data-testid'),
576
+ ].filter(Boolean).join(' '));
577
+ return label.includes('thinking') || label.includes('reasoning') || label.includes('pro thinking');
578
+ };
579
+ let panels;
580
+ try {
581
+ panels = document.querySelectorAll(
582
+ 'aside, [role="complementary"], [role="dialog"], [data-testid*="thinking"], [data-testid*="reasoning"], [class*="sidecar"], [class*="sidebar"]',
583
+ );
584
+ } catch { panels = []; }
585
+ for (const node of Array.from(panels)) {
586
+ if (!(node instanceof HTMLElement) || !isVisible(node)) continue;
587
+ const rect = node.getBoundingClientRect();
588
+ const rightSide = rect.left >= window.innerWidth * 0.35 && rect.width >= 180 && rect.height >= 120;
589
+ const panelLabel = norm([
590
+ node.getAttribute?.('aria-label'),
591
+ node.getAttribute?.('data-testid'),
592
+ node.className,
593
+ ].filter(Boolean).join(' '));
594
+ const verifiedThinkingPanel =
595
+ panelLabel.includes('thinking') ||
596
+ panelLabel.includes('reasoning') ||
597
+ panelLabel.includes('sidecar');
598
+ if ((hasBusyIndicator(node) || hasLiveProgress(node)) && verifiedThinkingPanel) return ${strong};
599
+ // A text-only sidecar match is intentionally weak: completed turns can retain a mounted
600
+ // reasoning panel whose shape/text heuristics still look active. The terminal gate may
601
+ // override only this weak evidence after a stable, debounced finished-action bar.
602
+ if (rightSide && looksLikeThinking(node)) return ${weak};
603
+ }
604
+ return ${idle};
605
+ };`;
606
+ }
607
+ export function buildThinkingActivePredicateJs(fnName) {
608
+ return buildThinkingActivityPredicateJs(fnName, false);
609
+ }
610
+ export function buildThinkingActivityDetailsPredicateJs(fnName) {
611
+ return buildThinkingActivityPredicateJs(fnName, true);
612
+ }
613
+ export async function readThinkingActivity(Runtime) {
614
+ try {
615
+ const { result } = await Runtime.evaluate({
616
+ expression: `(() => {
617
+ ${buildThinkingActivityDetailsPredicateJs("readThinkingActivity")}
618
+ return readThinkingActivity();
619
+ })()`,
620
+ returnByValue: true,
621
+ });
622
+ const value = result?.value;
623
+ return { active: Boolean(value?.active), strong: Boolean(value?.strong) };
624
+ }
625
+ catch {
626
+ return { active: false, strong: false };
627
+ }
628
+ }
629
+ export async function isThinkingActive(Runtime) {
630
+ return (await readThinkingActivity(Runtime)).active;
631
+ }
407
632
  export const startThinkingStatusMonitorForTest = startThinkingStatusMonitor;
408
633
  export const readThinkingStatusForTest = readThinkingStatus;
409
634
  export const buildThinkingStatusExpressionForTest = buildThinkingStatusExpression;
635
+ export const buildThinkingActivePredicateJsForTest = buildThinkingActivePredicateJs;
636
+ export const buildThinkingActivityDetailsPredicateJsForTest = buildThinkingActivityDetailsPredicateJs;
637
+ export { ACTIVE_THINKING_LABELS };
@@ -124,6 +124,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
124
124
  const modelButtonLiteral = JSON.stringify(MODEL_BUTTON_SELECTOR);
125
125
  const targetLevelLiteral = JSON.stringify(level.toLowerCase());
126
126
  const targetModelKindLiteral = JSON.stringify(inferThinkingTargetModelKind(desiredModel));
127
+ const targetIsGpt56ModelLiteral = JSON.stringify(/(?:^|[^0-9])5[._ -]6(?:[^0-9]|$)/i.test(desiredModel ?? ""));
127
128
  return `(async () => {
128
129
  ${buildClickDispatcher()}
129
130
 
@@ -132,13 +133,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
132
133
  const MODEL_BUTTON_SELECTOR = ${modelButtonLiteral};
133
134
  const TARGET_LEVEL = ${targetLevelLiteral};
134
135
  const TARGET_MODEL_KIND = ${targetModelKindLiteral};
136
+ const TARGET_IS_GPT56_MODEL = ${targetIsGpt56ModelLiteral};
135
137
 
136
138
  // Bilingual matchers: English level token + observed Chinese variants.
137
139
  const LEVEL_TOKENS = {
138
- light: ['light', 'instant', '轻'],
139
- standard: ['standard', 'medium', '标准'],
140
- extended: ['extended', 'high', '扩展', '深度', '加强'],
141
- heavy: ['heavy', 'extra high', '重度', '加重', ''],
140
+ light: ['light', 'instant', '轻', '极速'],
141
+ standard: ['standard', 'medium', '标准', '中'],
142
+ extended: ['extended', 'high', '扩展', '深度', '加强', '高'],
143
+ heavy: ['heavy', 'extra high', '重度', '加重', '极高'],
142
144
  };
143
145
  const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
144
146
 
@@ -166,6 +168,13 @@ function buildThinkingTimeExpression(level, desiredModel) {
166
168
  if (!token) return false;
167
169
  if (token === 'high') return hasToken(t, 'high') && !hasToken(t, 'extra');
168
170
  if (token === 'extra high') return hasToken(t, 'extra') && hasToken(t, 'high');
171
+ if (token === '极速') {
172
+ const suffix = t.slice(token.length);
173
+ return t === token || hasToken(t, token) || /^[0-9]/.test(suffix);
174
+ }
175
+ if (['中', '高', '极高'].includes(token)) {
176
+ return t === token || hasToken(t, token);
177
+ }
169
178
  return t === token || hasToken(t, token) || t.includes(token);
170
179
  });
171
180
  };
@@ -334,6 +343,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
334
343
  if (menu?.getAttribute?.('data-testid') === 'composer-intelligence-picker-content') {
335
344
  return true;
336
345
  }
346
+ if (menu?.querySelector?.(INTELLIGENCE_MENU_SELECTOR)) {
347
+ return true;
348
+ }
337
349
  const label = menu?.querySelector?.('.__menu-label, [class*="menu-label"]');
338
350
  return normalize(label?.textContent ?? '').includes('intelligence');
339
351
  };
@@ -347,6 +359,21 @@ function buildThinkingTimeExpression(level, desiredModel) {
347
359
  const items = Array.from(menu.querySelectorAll(MENU_ITEM_SELECTOR));
348
360
  const modelKind = modelKindOverride || effectiveTargetModelKind();
349
361
  if (modelKind === 'pro') {
362
+ // GPT-5.6's unified Intelligence picker exposes Pro as the highest
363
+ // effort radio directly. It no longer has a nested "Pro Extended"
364
+ // row, so preserve the legacy request semantics by selecting Pro.
365
+ if (
366
+ TARGET_LEVEL === 'extended' &&
367
+ isIntelligenceEffortMenu(menu) &&
368
+ !document.querySelector(PRO_EFFORT_TRIGGER_SELECTOR)
369
+ ) {
370
+ for (const item of items) {
371
+ const itemText = normalize(
372
+ (item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
373
+ );
374
+ if (itemText === 'pro') return item;
375
+ }
376
+ }
350
377
  for (const item of items) {
351
378
  const itemText = normalize(
352
379
  (item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
@@ -367,7 +394,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
367
394
  const itemText = normalize(
368
395
  (item.textContent ?? '') + ' ' + (item.getAttribute?.('aria-label') ?? ''),
369
396
  );
370
- if (modelKind && modelKind !== 'pro' && hasToken(itemText, 'pro')) {
397
+ if (modelKind !== 'pro' && hasToken(itemText, 'pro')) {
371
398
  continue;
372
399
  }
373
400
  if (
@@ -377,6 +404,16 @@ function buildThinkingTimeExpression(level, desiredModel) {
377
404
  return item;
378
405
  }
379
406
  }
407
+ if (TARGET_LEVEL === 'heavy') {
408
+ // Older Chinese layouts used bare 高 for the highest effort. Keep it
409
+ // only as a second-pass exact fallback so a current 高 row can never
410
+ // win before the primary 极高 row.
411
+ for (const item of items) {
412
+ const itemText = normalize(item.textContent ?? '');
413
+ const ariaLabel = normalize(item.getAttribute?.('aria-label') ?? '');
414
+ if (itemText === '高' || ariaLabel === '高') return item;
415
+ }
416
+ }
380
417
  return null;
381
418
  };
382
419
  const countEffortLevels = (menu) => {
@@ -390,6 +427,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
390
427
  const isEffortMenu = (menu) => {
391
428
  if (!isVisible(menu)) return false;
392
429
  if (menu.getAttribute?.('data-testid') === 'composer-intelligence-picker-content') return true;
430
+ if (menu.querySelector?.(INTELLIGENCE_MENU_SELECTOR)) return true;
393
431
  const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
394
432
  const labelText = normalize(label?.textContent ?? '');
395
433
  return (
@@ -450,8 +488,16 @@ function buildThinkingTimeExpression(level, desiredModel) {
450
488
  }
451
489
  return null;
452
490
  };
491
+ const freshComposerTrigger = (trigger) => {
492
+ if (!trigger?.matches?.('button.__composer-pill')) return null;
493
+ // React can replace the composer pill after an effort click. Keep using
494
+ // the captured node while it is live, but re-query once it is detached so
495
+ // verification does not read its stale pre-click label.
496
+ if (trigger.isConnected !== false) return trigger;
497
+ return findComposerEffortPill() || findModelButton() || trigger;
498
+ };
453
499
  const currentProEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
454
- const button = trigger?.matches?.('button.__composer-pill') ? trigger : findModelButton();
500
+ const button = freshComposerTrigger(trigger) || findModelButton();
455
501
  if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) !== 'pro') {
456
502
  return false;
457
503
  }
@@ -466,7 +512,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
466
512
  };
467
513
  const currentEffortPillMatchesTarget = (trigger, modelKindOverride = null) => {
468
514
  if (currentProEffortPillMatchesTarget(trigger, modelKindOverride)) return true;
469
- const button = trigger?.matches?.('button.__composer-pill') ? trigger : findModelButton();
515
+ const button = freshComposerTrigger(trigger) || findModelButton();
470
516
  if ((modelKindOverride || TARGET_MODEL_KIND || modelKindFromNode(button)) === 'pro') {
471
517
  return false;
472
518
  }
@@ -499,8 +545,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
499
545
  return { status: 'switched', label };
500
546
  }
501
547
 
502
- if (!refreshed && trigger && trigger.getAttribute?.('aria-expanded') !== 'true') {
503
- dispatchClickSequence(trigger);
548
+ const reopenTrigger = freshComposerTrigger(trigger) || trigger;
549
+ if (!refreshed && reopenTrigger?.getAttribute?.('aria-expanded') !== 'true') {
550
+ dispatchClickSequence(reopenTrigger);
504
551
  await sleep(INITIAL_WAIT_MS);
505
552
  }
506
553
  const deadline = performance.now() + 2000;
@@ -556,6 +603,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
556
603
  ];
557
604
  const findComposerEffortPill = () => {
558
605
  const seen = new Set();
606
+ let gpt56Fallback = null;
559
607
  for (const selector of COMPOSER_EFFORT_PILL_SELECTORS) {
560
608
  for (const button of document.querySelectorAll(selector)) {
561
609
  if (seen.has(button) || !isVisible(button)) continue;
@@ -574,9 +622,16 @@ function buildThinkingTimeExpression(level, desiredModel) {
574
622
  ) {
575
623
  return button;
576
624
  }
625
+ if (
626
+ TARGET_IS_GPT56_MODEL &&
627
+ button.matches?.('button.__composer-pill') &&
628
+ normalize(button.textContent ?? '') === 'pro'
629
+ ) {
630
+ gpt56Fallback ||= button;
631
+ }
577
632
  }
578
633
  }
579
- return null;
634
+ return gpt56Fallback;
580
635
  };
581
636
  let composerEffortPill = findComposerEffortPill();
582
637
  let modelBtn = findModelButton();
@@ -634,7 +689,9 @@ function buildThinkingTimeExpression(level, desiredModel) {
634
689
  }
635
690
  if (composerEffortPill) {
636
691
  if (attemptedModelButton && attemptedModelButton !== composerEffortPill) closeOpenMenus();
637
- const composerModelKind = TARGET_MODEL_KIND || modelKindFromNode(composerEffortPill);
692
+ const composerModelKind =
693
+ TARGET_MODEL_KIND ||
694
+ (TARGET_IS_GPT56_MODEL ? 'versioned' : modelKindFromNode(composerEffortPill));
638
695
  if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
639
696
  dispatchClickSequence(composerEffortPill);
640
697
  await sleep(INITIAL_WAIT_MS);
@@ -0,0 +1,78 @@
1
+ import { listRemoteChromeTargets } from "./chromeLifecycle.js";
2
+ import { verifyDevToolsReachable } from "./profileState.js";
3
+ /**
4
+ * Probe whether Chrome's DevTools endpoint (and optionally a specific target)
5
+ * is still reachable after a CDP client WebSocket disconnect.
6
+ */
7
+ export async function probeChromeTargetLiveness(options) {
8
+ const host = options.host || "127.0.0.1";
9
+ const port = options.port;
10
+ if (!Number.isFinite(port) || port <= 0) {
11
+ return { endpointReachable: false, targetFound: null, error: "missing debug port" };
12
+ }
13
+ const verifyEndpoint = options.verifyEndpoint ?? verifyDevToolsReachable;
14
+ const endpoint = await verifyEndpoint({ host, port, attempts: 2, timeoutMs: 1500 });
15
+ if (!endpoint.ok) {
16
+ return {
17
+ endpointReachable: false,
18
+ targetFound: null,
19
+ error: endpoint.error,
20
+ };
21
+ }
22
+ const targetId = options.targetId?.trim();
23
+ if (!targetId) {
24
+ return { endpointReachable: true, targetFound: null };
25
+ }
26
+ try {
27
+ const listTargets = options.listTargets ?? listRemoteChromeTargets;
28
+ const targets = await listTargets({
29
+ host,
30
+ port,
31
+ browserWSEndpoint: options.browserWSEndpoint,
32
+ });
33
+ const match = targets.find((target) => {
34
+ const id = target.targetId ?? target.id;
35
+ return id === targetId;
36
+ });
37
+ if (!match) {
38
+ return { endpointReachable: true, targetFound: false };
39
+ }
40
+ return {
41
+ endpointReachable: true,
42
+ targetFound: true,
43
+ matchedUrl: typeof match.url === "string" ? match.url : undefined,
44
+ };
45
+ }
46
+ catch (error) {
47
+ const message = error instanceof Error ? error.message : String(error);
48
+ // Endpoint answered /json/version; treat list failures as still recoverable.
49
+ return { endpointReachable: true, targetFound: null, error: message };
50
+ }
51
+ }
52
+ export function isRecoverableChromeDisconnect(liveness) {
53
+ if (!liveness.endpointReachable) {
54
+ return false;
55
+ }
56
+ // Confirmed live target → recoverable.
57
+ if (liveness.targetFound === true) {
58
+ return true;
59
+ }
60
+ // Confirmed missing target → not recoverable.
61
+ if (liveness.targetFound === false) {
62
+ return false;
63
+ }
64
+ // targetFound === null:
65
+ // - no target id was provided (endpoint-only check) → recoverable
66
+ // - target list failed after a specific id was requested (error set) → fail closed
67
+ return !liveness.error;
68
+ }
69
+ export function connectionLostUserMessage(options) {
70
+ if (options.recoverable) {
71
+ return options.remote
72
+ ? "Remote Chrome DevTools client disconnected before oracle finished; the browser target appears still alive."
73
+ : "Chrome DevTools client disconnected before oracle finished; the browser target appears still alive.";
74
+ }
75
+ return options.remote
76
+ ? "Remote Chrome connection lost before Oracle finished."
77
+ : "Chrome window closed before oracle finished. Please keep it open until completion.";
78
+ }