surf-cli 2.13.1 → 2.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.
@@ -18,9 +18,10 @@ const SELECTORS = {
18
18
  loginCta: 'a[href*="/auth/login"], button',
19
19
  sendButton:
20
20
  'button[data-testid="send-button"], button[data-testid*="composer-send"], form button[type="submit"]',
21
- modelButton: '[data-testid="model-switcher-dropdown-button"]',
21
+ modelButton:
22
+ '[data-testid="model-switcher-dropdown-button"], [data-testid="composer-footer-actions"] button[aria-haspopup="menu"], button.__composer-pill[aria-haspopup="menu"], .__composer-pill-composite button[aria-haspopup="menu"]',
22
23
  modelMenu: '[role="menu"][data-radix-menu-content]',
23
- modelMenuItem: '[role="menuitemradio"][data-testid^="model-switcher-"]',
24
+ modelMenuItem: 'button, [role="menuitem"], [role="menuitemradio"]',
24
25
  menuItemPrimaryLabel: '.min-w-0 > span',
25
26
  effortButton:
26
27
  '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"], button.__composer-pill[aria-haspopup="menu"], .__composer-pill-composite button[aria-haspopup="menu"]',
@@ -191,8 +192,8 @@ async function readPicker(cdp, kind, click = false) {
191
192
  ${buildClickDispatcher()}
192
193
  const kind = ${JSON.stringify(kind)};
193
194
  const nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
194
- if (kind === 'model') return true;
195
195
  const value = ((node.getAttribute?.('aria-label') || '') + ' ' + (node.textContent || '')).toLowerCase();
196
+ if (kind === 'model') return value.includes('gpt') || value.includes('pro') || value.includes('thinking') || value.includes('instant');
196
197
  return value.includes('thinking') || value.includes('pro');
197
198
  });
198
199
  const items = nodes.map((node) => {
@@ -224,13 +225,31 @@ async function readMenu(cdp, kind, allowSubmenu) {
224
225
  const choices = ${JSON.stringify(CHATGPT_EFFORT_CHOICES)};
225
226
  const containers = Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}));
226
227
  const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
227
- let menu = isModel ? containers[0] : containers.find((container) => {
228
+ let menu = isModel ? containers.find((container) => {
229
+ const labels = Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
230
+ .filter((item) => item.getAttribute?.('aria-haspopup') !== 'menu')
231
+ .map((item) => normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || '')));
232
+ return labels.some((label) => label.includes('gpt') || /^o[0-9]/.test(label));
233
+ }) : containers.find((container) => {
228
234
  const label = normalize(container.querySelector?.(${JSON.stringify(SELECTORS.effortMenuLabel)})?.textContent);
229
235
  const levels = new Set(Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
230
236
  .flatMap((item) => normalize(item.textContent).split(/\\s+/))
231
237
  .filter((word) => choices.includes(word)));
232
238
  return label.includes('thinking time') || levels.size >= 2;
233
239
  });
240
+ if (!menu && isModel && ${allowSubmenu}) {
241
+ for (const container of containers) {
242
+ const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
243
+ .find((item) => {
244
+ const label = normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || ''));
245
+ return label.includes('model') || label.includes('advanced');
246
+ });
247
+ if (trigger) {
248
+ dispatchClickSequence(trigger);
249
+ return { found: false, submenuOpened: true, items: [] };
250
+ }
251
+ }
252
+ }
234
253
  if (!menu && !isModel && ${allowSubmenu}) {
235
254
  for (const container of containers) {
236
255
  const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
@@ -299,7 +318,7 @@ async function clickMenuItem(cdp, kind, match) {
299
318
  .replace(/\\s+/g, ' ').trim().slice(0, 80);
300
319
  return label === expectedLabel;
301
320
  });
302
- return matches.length === 1 ? dispatchClickSequence(matches[0]) : false;
321
+ return matches.length >= 1 ? dispatchClickSequence(matches[0]) : false;
303
322
  })()`,
304
323
  );
305
324
  }
@@ -317,8 +336,17 @@ async function selectModel(cdp, desiredModel, timeoutMs = 8000, signal) {
317
336
  await delay(200, signal);
318
337
  const state = await readPicker(cdp, "model");
319
338
  const verified = verifyChatGPTModelSelection(state?.items, desiredModel);
320
- if (!verified) throw verificationError("model", desiredModel, menu.items);
321
- return verified.displayLabel || verified.label;
339
+ if (verified) return verified.displayLabel || verified.label;
340
+
341
+ const reopened = await readPicker(cdp, "model", true);
342
+ if (reopened?.items?.length !== 1) throw verificationError("model", desiredModel, menu.items);
343
+ const readbackMenu = await waitForMenu(cdp, "model", timeoutMs, signal);
344
+ const readbackVerified = verifyChatGPTModelSelection(
345
+ readbackMenu.items.filter((item) => item.selected),
346
+ desiredModel,
347
+ );
348
+ if (!readbackVerified) throw verificationError("model", desiredModel, readbackMenu.items);
349
+ return readbackVerified.label;
322
350
  }
323
351
 
324
352
  async function selectEffort(cdp, desiredEffort, timeoutMs = 8000, signal) {