surf-cli 2.17.0 → 2.19.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.
@@ -1,6 +1,7 @@
1
1
  const { abortableDelay, throwIfAborted } = require("./abort.cjs");
2
2
  const {
3
3
  CHATGPT_EFFORT_CHOICES,
4
+ CHATGPT_EFFORT_VALUE,
4
5
  boundedOptionLabels,
5
6
  normalizeChatGPTEffortChoice,
6
7
  normalizeChatGPTModelChoice,
@@ -495,243 +496,356 @@ function verificationError(kind, requested, items = [], invalid = false) {
495
496
  return error;
496
497
  }
497
498
 
498
- async function readPicker(cdp, kind, click = false) {
499
- const selector = kind === "model" ? SELECTORS.modelButton : SELECTORS.effortButton;
500
- return evaluate(
501
- cdp,
502
- `(() => {
503
- ${buildClickDispatcher()}
504
- const kind = ${JSON.stringify(kind)};
505
- const effortChoices = new Set(${JSON.stringify(CHATGPT_EFFORT_CHOICES)});
506
- const effortOwnerLabels = new Set([...effortChoices, 'thinking']);
507
- const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
508
- const labelFor = (node) => {
509
- const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
510
- .split(/\s+/)
511
- .map((id) => document.getElementById(id)?.textContent || '')
512
- .join(' ');
513
- const text = (node.innerText || node.textContent || '').replace(/\s+/g, ' ').trim();
514
- const aria = (node.getAttribute?.('aria-label') || '').replace(/\s+/g, ' ').trim();
515
- const title = (node.getAttribute?.('title') || '').replace(/\s+/g, ' ').trim();
516
- return [text, aria, labelledBy, title].filter(Boolean).join(' | ');
517
- };
518
- let nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
519
- const value = normalize(labelFor(node));
520
- if (kind !== 'model') {
521
- const words = value.split(/\s+/).filter(Boolean);
522
- const hasEffort = words.some((word) => effortOwnerLabels.has(word));
523
- const looksLikeModel = node.getAttribute?.('data-testid') === 'model-switcher-dropdown-button' ||
524
- value.includes('current model') || value.includes('gpt') || value.includes('instant');
525
- return hasEffort && !looksLikeModel;
526
- }
527
- return value.includes('gpt') || value.includes('thinking') || value.includes('instant');
528
- });
529
- if (kind === 'model' && nodes.length === 0) {
530
- nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
531
- const value = normalize(labelFor(node));
532
- return value.includes('pro');
533
- });
534
- }
535
- if (kind === 'model' && nodes.length === 0) {
536
- nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) =>
537
- node.getAttribute?.('aria-haspopup') === 'menu' || node.getAttribute?.('aria-expanded') !== null
538
- );
499
+
500
+ function combinedPickerScript(click = false) {
501
+ return `(() => {
502
+ ${buildClickDispatcher()}
503
+ const pickerSelector = ${JSON.stringify(SELECTORS.modelButton)};
504
+ const normalizeText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
505
+ const visible = (node) => {
506
+ if (!node || node.nodeType !== 1) return false;
507
+ for (let el = node; el && el.nodeType === 1; el = el.parentElement) {
508
+ if (el.hasAttribute?.('hidden') || el.hasAttribute?.('inert') || el.getAttribute?.('aria-hidden') === 'true') return false;
509
+ const style = window.getComputedStyle?.(el);
510
+ if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
539
511
  }
540
- const items = nodes.map((node) => {
541
- const text = (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim();
542
- const aria = (node.getAttribute?.('aria-label') || '').replace(/\\s+/g, ' ').trim();
543
- const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
544
- .split(/\\s+/)
545
- .map((id) => document.getElementById(id)?.textContent || '')
546
- .join(' ')
547
- .replace(/\\s+/g, ' ')
548
- .trim();
549
- const title = (node.getAttribute?.('title') || '').replace(/\\s+/g, ' ').trim();
512
+ const rect = node.getBoundingClientRect?.();
513
+ return !rect || (rect.width > 0 && rect.height > 0);
514
+ };
515
+ const labelledText = (node) => {
516
+ const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
517
+ .split(/\\s+/)
518
+ .map((id) => document.getElementById(id)?.textContent || '')
519
+ .join(' ');
520
+ return [node.innerText || node.textContent || '', node.getAttribute?.('aria-label') || '', labelledBy, node.getAttribute?.('title') || '']
521
+ .map(normalizeText)
522
+ .filter(Boolean)
523
+ .join(' | ');
524
+ };
525
+ const directSpanText = (node) => Array.from(node?.children || [])
526
+ .filter((child) => child.tagName === 'SPAN')
527
+ .filter(visible)
528
+ .map((span) => normalizeText(span.innerText || span.textContent || ''))
529
+ .filter(Boolean)
530
+ .filter((label, index, labels) => labels.indexOf(label) === index)
531
+ .join(' ');
532
+ const itemFor = (node, fallbackLabel) => {
533
+ const text = normalizeText(directSpanText(node) || node.innerText || node.textContent || '');
534
+ const aria = normalizeText(node.getAttribute?.('aria-label') || '');
535
+ const labelled = labelledText(node);
536
+ const label = [text, aria, labelled]
537
+ .filter(Boolean)
538
+ .filter((value, index, values) => values.indexOf(value) === index)
539
+ .join(' | ');
540
+ const modelText = text.toLowerCase();
541
+ const modelKey = /\\b5\\.6\\b/.test(modelText) && /\\bsol\\b/.test(modelText)
542
+ ? 'gpt56sol'
543
+ : /\\b5\\.5\\b/.test(modelText)
544
+ ? 'gpt55'
545
+ : (/\\bgpt\\s*6\\b/.test(modelText) || /^6(?:\\s|$)/.test(modelText))
546
+ ? 'gpt6astra'
547
+ : /^latest$/.test(modelText)
548
+ ? 'latest'
549
+ : null;
550
+ return {
551
+ role: node.getAttribute?.('role') || (node.tagName === 'BUTTON' ? 'button' : null),
552
+ label: (label || fallbackLabel || '').slice(0, 240),
553
+ displayLabel: (text || aria || fallbackLabel || '').slice(0, 80),
554
+ testId: node.getAttribute?.('data-testid') || null,
555
+ ...(modelKey ? { modelKey } : {}),
556
+ };
557
+ };
558
+ const buttons = Array.from(document.querySelectorAll(pickerSelector)).filter((node) =>
559
+ visible(node) && (node.getAttribute?.('aria-haspopup') === 'menu' || node.getAttribute?.('aria-expanded') !== null)
560
+ );
561
+ const pickerButtons = buttons.map((node) => itemFor(node));
562
+ if (${click} && buttons.length === 1) dispatchClickSequence(buttons[0]);
563
+
564
+ const menus = Array.from(document.querySelectorAll('[role="menu"][data-radix-menu-content]')).filter(visible);
565
+ const menu = menus.find((node) => node.querySelector?.('[data-testid="composer-intelligence-picker-content"]')) || menus[0] || null;
566
+ if (!menu) return { pickerButtons, menuFound: false, modelItems: pickerButtons, effortItems: [] };
567
+
568
+ const content = menu.querySelector?.('[data-testid="composer-intelligence-picker-content"]') || menu;
569
+ const panels = Array.from(content.querySelectorAll?.('[data-testid="composer-model-picker-slider-simple-view"], [data-testid="composer-model-picker-slider-advanced-view"], [data-view="simple"], [data-view="advanced"]') || [])
570
+ .filter(visible);
571
+ const activePanel = panels.find((panel) => !panel.hasAttribute?.('inert')) || content;
572
+ const activeId = activePanel.getAttribute?.('data-testid') || '';
573
+ const view = activePanel.getAttribute?.('data-view') || (activeId.includes('advanced') ? 'advanced' : 'simple');
574
+
575
+ const modelToggle = Array.from(content.querySelectorAll?.('[role="menuitem"][aria-label="Select model"]') || [])
576
+ .filter(visible)[0] || null;
577
+ const modelToggleItem = modelToggle ? itemFor(modelToggle) : null;
578
+ const modelOptions = Array.from(activePanel.querySelectorAll?.('[role="menuitemradio"]') || [])
579
+ .filter(visible)
580
+ .map((node) => {
581
+ const item = itemFor(node);
582
+ const state = String(node.getAttribute?.('data-state') || '').toLowerCase();
550
583
  return {
551
- role: node.getAttribute?.('role') || (node.tagName === 'BUTTON' ? 'button' : null),
552
- label: [text, aria, labelledBy, title].filter(Boolean).join(' | ').slice(0, 240),
553
- displayLabel: (text || aria || labelledBy || title).slice(0, 80),
554
- testId: node.getAttribute?.('data-testid') || null,
584
+ ...item,
585
+ selected: node.getAttribute?.('aria-checked') === 'true' || state === 'checked',
555
586
  };
556
- });
557
- if (${click} && nodes.length === 1) dispatchClickSequence(nodes[0]);
558
- return { items };
559
- })()`,
560
- );
587
+ })
588
+ .filter((item) => /\\b(latest|gpt|5\\.6|5\\.5)\\b/i.test(item.label));
589
+
590
+ const power = Array.from(content.querySelectorAll?.('[role="menuitem"][aria-label="Power"]') || [])
591
+ .filter(visible)[0] || null;
592
+ let effortItems = [];
593
+ if (power) {
594
+ const slider = power.querySelector?.('[data-model-reasoning-effort-slider] [role="slider"]') || power.querySelector?.('[role="slider"]');
595
+ if (slider) {
596
+ const describedBy = String(power.getAttribute?.('aria-describedby') || slider.getAttribute?.('aria-describedby') || '')
597
+ .split(/\\s+/)
598
+ .map((id) => normalizeText(document.getElementById(id)?.textContent || ''))
599
+ .filter(Boolean)
600
+ .join(' | ');
601
+ const ariaValueText = normalizeText(slider.getAttribute?.('aria-valuetext') || '');
602
+ const powerText = normalizeText(power.innerText || power.textContent || '');
603
+ effortItems = [{
604
+ role: 'slider',
605
+ label: [ariaValueText, describedBy, powerText].filter(Boolean).join(' | ').slice(0, 240),
606
+ displayLabel: (ariaValueText || describedBy || powerText).slice(0, 80),
607
+ value: Number(slider.getAttribute?.('aria-valuenow')),
608
+ min: Number(slider.getAttribute?.('aria-valuemin')),
609
+ max: Number(slider.getAttribute?.('aria-valuemax')),
610
+ selected: true,
611
+ }];
612
+ }
613
+ }
614
+
615
+ return {
616
+ pickerButtons,
617
+ menuFound: true,
618
+ view,
619
+ modelToggle: modelToggleItem,
620
+ modelToggleExpanded: modelToggle?.getAttribute?.('aria-expanded') === 'true',
621
+ modelOptions,
622
+ modelItems: [...(modelToggleItem ? [modelToggleItem] : []), ...modelOptions.filter((item) => item.selected)],
623
+ effortItems,
624
+ };
625
+ })()`;
626
+ }
627
+
628
+ async function readCombinedPicker(cdp, click = false, signal) {
629
+ return evaluate(cdp, combinedPickerScript(click), signal);
561
630
  }
562
631
 
563
- async function readMenu(cdp, kind, allowSubmenu) {
564
- const isModel = kind === "model";
565
- const menuSelector = isModel ? SELECTORS.modelMenu : SELECTORS.effortMenu;
566
- const itemSelector = isModel ? SELECTORS.modelMenuItem : SELECTORS.effortMenuItem;
632
+ async function waitForCombinedMenu(cdp, timeoutMs, signal) {
633
+ const deadline = Date.now() + timeoutMs;
634
+ while (Date.now() < deadline) {
635
+ const result = await readCombinedPicker(cdp, false, signal);
636
+ if (result?.menuFound) return result;
637
+ await delay(100, signal);
638
+ }
639
+ return { menuFound: false, modelItems: [], modelOptions: [], effortItems: [] };
640
+ }
641
+
642
+ async function activateAdvancedModelView(cdp, signal) {
567
643
  return evaluate(
568
644
  cdp,
569
645
  `(() => {
570
646
  ${buildClickDispatcher()}
571
- const isModel = ${isModel};
572
- const choices = ${JSON.stringify(CHATGPT_EFFORT_CHOICES)};
573
- const containers = Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}));
574
- const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
575
- let menu = isModel ? containers.find((container) => {
576
- const labels = Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
577
- .filter((item) => item.getAttribute?.('aria-haspopup') !== 'menu')
578
- .map((item) => normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || '')));
579
- return labels.some((label) => label.includes('gpt') || /^o[0-9]/.test(label));
580
- }) : containers.find((container) => {
581
- const label = normalize(container.querySelector?.(${JSON.stringify(SELECTORS.effortMenuLabel)})?.textContent);
582
- const levels = new Set(Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
583
- .flatMap((item) => normalize(item.textContent).split(/\\s+/))
584
- .filter((word) => choices.includes(word)));
585
- return label.includes('thinking time') || levels.size >= 2;
586
- });
587
- if (!menu && isModel && ${allowSubmenu}) {
588
- for (const container of containers) {
589
- const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
590
- .find((item) => {
591
- const label = normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || ''));
592
- return label.includes('model') || label.includes('advanced');
593
- });
594
- if (trigger) {
595
- dispatchClickSequence(trigger);
596
- return { found: false, submenuOpened: true, items: [] };
597
- }
598
- }
599
- }
600
- if (!menu && !isModel && ${allowSubmenu}) {
601
- for (const container of containers) {
602
- const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
603
- .find((item) => {
604
- const label = normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || ''));
605
- return label.includes('thinking time') || label.includes('reasoning effort');
606
- });
607
- if (trigger) {
608
- dispatchClickSequence(trigger);
609
- return { found: false, submenuOpened: true, items: [] };
610
- }
647
+ const visible = (node) => {
648
+ if (!node || node.nodeType !== 1) return false;
649
+ for (let el = node; el && el.nodeType === 1; el = el.parentElement) {
650
+ if (el.hasAttribute?.('hidden') || el.hasAttribute?.('inert') || el.getAttribute?.('aria-hidden') === 'true') return false;
651
+ const style = window.getComputedStyle?.(el);
652
+ if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
611
653
  }
612
- }
613
- if (!menu) return { found: false, items: [] };
614
- const items = Array.from(menu.querySelectorAll(${JSON.stringify(itemSelector)})).map((item) => {
615
- const primary = isModel ? item.querySelector?.(${JSON.stringify(SELECTORS.menuItemPrimaryLabel)}) : null;
616
- const label = (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '')
617
- .replace(/\\s+/g, ' ').trim().slice(0, 80);
618
- const state = (item.getAttribute?.('data-state') || '').toLowerCase();
619
- const selected = item.getAttribute?.('aria-checked') === 'true' ||
620
- item.getAttribute?.('aria-selected') === 'true' || item.getAttribute?.('data-selected') === 'true' ||
621
- ['checked', 'selected', 'on'].includes(state) ||
622
- Boolean(item.querySelector?.(${JSON.stringify(SELECTORS.selectedMenuIndicator)}));
623
- return {
624
- role: item.getAttribute?.('role') || (item.tagName === 'BUTTON' ? 'button' : null),
625
- label,
626
- testId: item.getAttribute?.('data-testid') || null,
627
- selected,
628
- };
629
- });
630
- return { found: true, items };
654
+ const rect = node.getBoundingClientRect?.();
655
+ return !rect || (rect.width > 0 && rect.height > 0);
656
+ };
657
+ const trigger = Array.from(document.querySelectorAll('[role="menuitem"][aria-label="Select model"]')).filter(visible)[0];
658
+ if (!trigger) return false;
659
+ if (trigger.getAttribute?.('aria-expanded') === 'true') return true;
660
+ return dispatchClickSequence(trigger);
631
661
  })()`,
662
+ signal,
632
663
  );
633
664
  }
634
665
 
635
- async function waitForMenu(cdp, kind, timeoutMs, signal) {
636
- const deadline = Date.now() + timeoutMs;
637
- let allowSubmenu = true;
638
- while (Date.now() < deadline) {
639
- const result = await readMenu(cdp, kind, allowSubmenu);
640
- if (result?.found) return result;
641
- if (result?.submenuOpened) allowSubmenu = false;
642
- await delay(100, signal);
643
- }
644
- return { found: false, items: [] };
645
- }
646
-
647
- async function clickMenuItem(cdp, kind, match) {
648
- const isModel = kind === "model";
649
- const menuSelector = isModel ? SELECTORS.modelMenu : SELECTORS.effortMenu;
650
- const itemSelector = isModel ? SELECTORS.modelMenuItem : SELECTORS.effortMenuItem;
666
+ async function clickModelOption(cdp, match, signal) {
651
667
  return evaluate(
652
668
  cdp,
653
669
  `(() => {
654
670
  ${buildClickDispatcher()}
655
671
  const expectedTestId = ${JSON.stringify(match.testId)};
656
672
  const expectedLabel = ${JSON.stringify(match.label)};
657
- const items = Array.from(new Set(
658
- Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}))
659
- .flatMap((menu) => Array.from(menu.querySelectorAll(${JSON.stringify(itemSelector)}))),
660
- ));
661
- const matches = items.filter((item) => {
673
+ const normalizeText = (value) => String(value || '').replace(/\\s+/g, ' ').trim();
674
+ const visible = (node) => {
675
+ if (!node || node.nodeType !== 1) return false;
676
+ for (let el = node; el && el.nodeType === 1; el = el.parentElement) {
677
+ if (el.hasAttribute?.('hidden') || el.hasAttribute?.('inert') || el.getAttribute?.('aria-hidden') === 'true') return false;
678
+ const style = window.getComputedStyle?.(el);
679
+ if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
680
+ }
681
+ const rect = node.getBoundingClientRect?.();
682
+ return !rect || (rect.width > 0 && rect.height > 0);
683
+ };
684
+ const menu = Array.from(document.querySelectorAll('[role="menu"][data-radix-menu-content]')).filter(visible)[0];
685
+ const content = menu?.querySelector?.('[data-testid="composer-intelligence-picker-content"]') || menu;
686
+ const panels = Array.from(content?.querySelectorAll?.('[data-testid="composer-model-picker-slider-advanced-view"], [data-view="advanced"]') || []).filter(visible);
687
+ const panel = panels.find((node) => !node.hasAttribute?.('inert')) || content;
688
+ const matches = Array.from(panel?.querySelectorAll?.('[role="menuitemradio"]') || []).filter(visible).filter((item) => {
662
689
  if (expectedTestId) return item.getAttribute?.('data-testid') === expectedTestId;
663
- const primary = ${isModel} ? item.querySelector?.(${JSON.stringify(SELECTORS.menuItemPrimaryLabel)}) : null;
664
- const label = (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '')
665
- .replace(/\\s+/g, ' ').trim().slice(0, 80);
690
+ const label = normalizeText(item.innerText || item.textContent || item.getAttribute?.('aria-label') || '').slice(0, 240);
666
691
  return label === expectedLabel;
667
692
  });
668
- return matches.length >= 1 ? dispatchClickSequence(matches[0]) : false;
693
+ return matches.length === 1 ? dispatchClickSequence(matches[0]) : false;
669
694
  })()`,
695
+ signal,
670
696
  );
671
697
  }
672
698
 
673
- async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
699
+ async function closeCombinedPicker(cdp, inputCdp, signal) {
700
+ const hasMenu = (await readCombinedPicker(cdp, false, signal))?.menuFound;
701
+ if (!hasMenu) return;
702
+ if (inputCdp) {
703
+ await inputCdp("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
704
+ await inputCdp("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27 });
705
+ } else {
706
+ await evaluate(cdp, `document.activeElement?.blur?.(); document.body?.click?.(); true`, signal);
707
+ }
708
+ const deadline = Date.now() + 1000;
709
+ while (Date.now() < deadline) {
710
+ await delay(50, signal);
711
+ if (!(await readCombinedPicker(cdp, false, signal))?.menuFound) return;
712
+ }
713
+ throw uiError("picker_close_failed", "ChatGPT picker did not close after Escape/outside click");
714
+ }
715
+
716
+ async function verifyCurrentModel(cdp, inputCdp, desiredModel, timeoutMs = 8000, signal) {
674
717
  throwIfAborted(signal);
675
- const picker = await readPicker(cdp, "model", true);
676
- if (picker?.items?.length !== 1) throw verificationError("model", desiredModel);
677
- await delay(300, signal);
678
- const menu = await waitForMenu(cdp, "model", timeoutMs, signal);
679
- const currentAdvancedModel = verifyChatGPTModelSelection(
680
- menu.items.filter((item) => /\bmodel\b/i.test(String(item?.label || ""))),
681
- desiredModel,
682
- );
683
- if (currentAdvancedModel) return currentAdvancedModel.displayLabel || currentAdvancedModel.label;
718
+ const state = await readCombinedPicker(cdp, false, signal);
719
+ const verified = verifyChatGPTModelSelection(state?.modelItems, desiredModel);
720
+ if (verified) {
721
+ await closeCombinedPicker(cdp, inputCdp, signal);
722
+ return verified.displayLabel || verified.label;
723
+ }
724
+ let menu = state?.menuFound ? state : null;
725
+ if (!menu) {
726
+ const opened = await readCombinedPicker(cdp, true, signal);
727
+ if (opened?.pickerButtons?.length !== 1) throw verificationError("model", desiredModel, state?.modelItems);
728
+ await delay(150, signal);
729
+ menu = await waitForCombinedMenu(cdp, timeoutMs, signal);
730
+ }
731
+ let readback = verifyChatGPTModelSelection(menu.modelItems, desiredModel);
732
+ if (!readback) {
733
+ if (!(await activateAdvancedModelView(cdp, signal))) throw verificationError("model", desiredModel, menu.modelItems);
734
+ await delay(150, signal);
735
+ menu = await waitForCombinedMenu(cdp, timeoutMs, signal);
736
+ readback = verifyChatGPTModelSelection(menu.modelItems, desiredModel);
737
+ }
738
+ if (!readback) throw verificationError("model", desiredModel, menu.modelItems);
739
+ await closeCombinedPicker(cdp, inputCdp, signal);
740
+ return readback.displayLabel || readback.label;
741
+ }
742
+
743
+ async function selectModel(cdp, inputCdp, desiredModel, timeoutMs = 8000, signal) {
744
+ throwIfAborted(signal);
745
+ const picker = await readCombinedPicker(cdp, true, signal);
746
+ if (picker?.pickerButtons?.length !== 1) throw verificationError("model", desiredModel);
747
+ await delay(200, signal);
748
+ let menu = await waitForCombinedMenu(cdp, timeoutMs, signal);
749
+ const current = verifyChatGPTModelSelection(menu.modelItems, desiredModel);
750
+ if (current) {
751
+ await closeCombinedPicker(cdp, inputCdp, signal);
752
+ return current.displayLabel || current.label;
753
+ }
684
754
 
685
- const match = resolveChatGPTModelMenuOption(menu.items, desiredModel);
686
- if (!match || !(await clickMenuItem(cdp, "model", match))) {
687
- throw verificationError("model", desiredModel, menu.items);
755
+ if (!(await activateAdvancedModelView(cdp, signal))) throw verificationError("model", desiredModel, menu.modelItems);
756
+ await delay(150, signal);
757
+ menu = await waitForCombinedMenu(cdp, timeoutMs, signal);
758
+ const selectedCurrent = verifyChatGPTModelSelection(menu.modelItems, desiredModel);
759
+ if (selectedCurrent) {
760
+ await closeCombinedPicker(cdp, inputCdp, signal);
761
+ return selectedCurrent.displayLabel || selectedCurrent.label;
762
+ }
763
+ const match = resolveChatGPTModelMenuOption(menu.modelOptions, desiredModel);
764
+ if (!match || !(await clickModelOption(cdp, match, signal))) {
765
+ throw verificationError("model", desiredModel, menu.modelOptions);
688
766
  }
689
767
  await delay(200, signal);
690
- const state = await readPicker(cdp, "model");
691
- const verified = verifyChatGPTModelSelection(state?.items, desiredModel);
692
- if (verified) return verified.displayLabel || verified.label;
693
-
694
- const reopened = await readPicker(cdp, "model", true);
695
- if (reopened?.items?.length !== 1) throw verificationError("model", desiredModel, menu.items);
696
- const readbackMenu = await waitForMenu(cdp, "model", timeoutMs, signal);
697
- const readbackVerified = verifyChatGPTModelSelection(
698
- readbackMenu.items.filter((item) => item.selected),
699
- desiredModel,
768
+ return verifyCurrentModel(cdp, inputCdp, desiredModel, timeoutMs, signal);
769
+ }
770
+
771
+ async function focusPowerControl(cdp, signal) {
772
+ return evaluate(
773
+ cdp,
774
+ `(() => {
775
+ const visible = (node) => {
776
+ if (!node || node.nodeType !== 1) return false;
777
+ for (let el = node; el && el.nodeType === 1; el = el.parentElement) {
778
+ if (el.hasAttribute?.('hidden') || el.hasAttribute?.('inert') || el.getAttribute?.('aria-hidden') === 'true') return false;
779
+ const style = window.getComputedStyle?.(el);
780
+ if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
781
+ }
782
+ const rect = node.getBoundingClientRect?.();
783
+ return !rect || (rect.width > 0 && rect.height > 0);
784
+ };
785
+ const power = Array.from(document.querySelectorAll('[role="menuitem"][aria-label="Power"]')).filter(visible)[0];
786
+ if (!power) return false;
787
+ power.focus?.();
788
+ return true;
789
+ })()`,
790
+ signal,
700
791
  );
701
- if (!readbackVerified) throw verificationError("model", desiredModel, readbackMenu.items);
702
- return readbackVerified.label;
703
792
  }
704
793
 
705
- async function selectEffort(cdp, desiredEffort, timeoutMs = 8000, signal) {
794
+ async function dispatchEffortArrow(cdp, inputCdp, key, signal) {
795
+ if (inputCdp) {
796
+ const code = key === "ArrowRight" ? 39 : 37;
797
+ await inputCdp("Input.dispatchKeyEvent", { type: "keyDown", key, code: key, windowsVirtualKeyCode: code });
798
+ await inputCdp("Input.dispatchKeyEvent", { type: "keyUp", key, code: key, windowsVirtualKeyCode: code });
799
+ return;
800
+ }
801
+ await evaluate(cdp, `(() => {
802
+ const key = ${JSON.stringify(key)};
803
+ const power = document.activeElement || Array.from(document.querySelectorAll('[role="menuitem"][aria-label="Power"]'))[0];
804
+ power?.dispatchEvent?.(new KeyboardEvent('keydown', { key, code: key, bubbles: true, cancelable: true }));
805
+ power?.dispatchEvent?.(new KeyboardEvent('keyup', { key, code: key, bubbles: true, cancelable: true }));
806
+ return true;
807
+ })()`, signal);
808
+ }
809
+
810
+ function strictVerifiedEffort(items, desiredEffort) {
811
+ const verified = verifyChatGPTEffortSelection(items, desiredEffort);
812
+ if (!verified) return null;
813
+ return verified.min === 0 && verified.max === 4 && Number.isInteger(verified.value) ? verified : null;
814
+ }
815
+
816
+ async function selectEffort(cdp, inputCdp, desiredEffort, timeoutMs = 8000, signal) {
706
817
  throwIfAborted(signal);
707
818
  const normalizedEffort = normalizeChatGPTEffortChoice(desiredEffort);
708
819
  if (!normalizedEffort) throw verificationError("effort", desiredEffort, [], true);
709
- const current = await readPicker(cdp, "effort");
710
- const currentVerified = verifyChatGPTEffortSelection(current?.items, normalizedEffort);
711
- if (currentVerified) return currentVerified.displayLabel || currentVerified.label;
820
+ const targetValue = CHATGPT_EFFORT_VALUE.get(normalizedEffort);
712
821
 
713
- const picker = await readPicker(cdp, "effort", true);
714
- if (picker?.items?.length !== 1) throw verificationError("effort", desiredEffort);
715
- await delay(300, signal);
716
- const menu = await waitForMenu(cdp, "effort", timeoutMs, signal);
717
- const match = resolveChatGPTEffortMenuOption(menu.items, normalizedEffort);
718
- if (!match || !(await clickMenuItem(cdp, "effort", match))) {
719
- throw verificationError("effort", desiredEffort, menu.items);
720
- }
822
+ const picker = await readCombinedPicker(cdp, true, signal);
823
+ if (picker?.pickerButtons?.length !== 1) throw verificationError("effort", desiredEffort);
721
824
  await delay(200, signal);
722
- const pillState = await readPicker(cdp, "effort");
723
- const pillVerified = verifyChatGPTEffortSelection(pillState?.items, normalizedEffort);
724
- if (pillVerified) return pillVerified.displayLabel || pillVerified.label;
725
-
726
- const reopened = await readPicker(cdp, "effort", true);
727
- if (reopened?.items?.length !== 1) throw verificationError("effort", desiredEffort, menu.items);
728
- const readbackMenu = await waitForMenu(cdp, "effort", timeoutMs, signal);
729
- const verified = verifyChatGPTEffortSelection(
730
- readbackMenu.items.filter((item) => item.selected),
731
- normalizedEffort,
732
- );
733
- if (!verified) throw verificationError("effort", desiredEffort, readbackMenu.items);
734
- return verified.label;
825
+ let menu = await waitForCombinedMenu(cdp, timeoutMs, signal);
826
+ const current = strictVerifiedEffort(menu.effortItems, normalizedEffort);
827
+ if (current) {
828
+ await closeCombinedPicker(cdp, inputCdp, signal);
829
+ return current.displayLabel || current.label;
830
+ }
831
+ const effort = menu.effortItems?.[0];
832
+ if (!effort || effort.min !== 0 || effort.max !== 4 || !Number.isInteger(effort.value) || !Number.isInteger(targetValue)) {
833
+ throw verificationError("effort", desiredEffort, menu.effortItems);
834
+ }
835
+ if (!verifyChatGPTEffortSelection(menu.effortItems, CHATGPT_EFFORT_CHOICES[effort.value])) {
836
+ throw verificationError("effort", desiredEffort, menu.effortItems);
837
+ }
838
+ if (!(await focusPowerControl(cdp, signal))) throw verificationError("effort", desiredEffort, menu.effortItems);
839
+ const direction = targetValue > effort.value ? "ArrowRight" : "ArrowLeft";
840
+ for (let index = 0; index < Math.abs(targetValue - effort.value); index += 1) {
841
+ await dispatchEffortArrow(cdp, inputCdp, direction, signal);
842
+ await delay(100, signal);
843
+ }
844
+ menu = await waitForCombinedMenu(cdp, timeoutMs, signal);
845
+ const verified = strictVerifiedEffort(menu.effortItems, normalizedEffort);
846
+ if (!verified) throw verificationError("effort", desiredEffort, menu.effortItems);
847
+ await closeCombinedPicker(cdp, inputCdp, signal);
848
+ return verified.displayLabel || verified.label;
735
849
  }
736
850
 
737
851
  async function typePrompt(cdp, inputCdp, prompt, signal) {
@@ -867,6 +981,7 @@ module.exports = {
867
981
  selectEffort,
868
982
  selectGitHubTool,
869
983
  selectModel,
984
+ verifyCurrentModel,
870
985
  verifyChatGPTEffortSelection,
871
986
  verifyChatGPTModelSelection,
872
987
  waitForChatGPTAttachment,
@@ -14,6 +14,7 @@ const {
14
14
  selectChatTab,
15
15
  selectGitHubTool,
16
16
  selectModel,
17
+ verifyCurrentModel,
17
18
  typePrompt,
18
19
  verifyChatGPTEffortSelection,
19
20
  verifyChatGPTModelSelection,
@@ -165,7 +166,7 @@ async function dispatch(options) {
165
166
  let modelVerified = null;
166
167
  let effortVerified = null;
167
168
  if (model) {
168
- modelVerified = await selectModel(cdp, model, 8000, signal);
169
+ modelVerified = await selectModel(cdp, inputCdp, model, 8000, signal);
169
170
  log(`Verified model: ${modelVerified}`);
170
171
  }
171
172
  if (file) {
@@ -214,9 +215,13 @@ async function dispatch(options) {
214
215
  await typePrompt(cdp, inputCdp, prompt, signal);
215
216
  log("Prompt typed");
216
217
  if (effort) {
217
- effortVerified = await selectEffort(cdp, effort, 8000, signal);
218
+ effortVerified = await selectEffort(cdp, inputCdp, effort, 8000, signal);
218
219
  log(`Verified effort: ${effortVerified}`);
219
220
  }
221
+ if (model) {
222
+ modelVerified = await verifyCurrentModel(cdp, inputCdp, model, 8000, signal);
223
+ log(`Reverified model before submit: ${modelVerified}`);
224
+ }
220
225
  const baseline = normalizeResponseSnapshot(await readChatGPTResponseSnapshot(cdp));
221
226
  if (beforeSubmit) await raceAbort(beforeSubmit, signal);
222
227
  await clickSend(cdp, inputCdp, signal);