surf-cli 2.16.1 → 2.18.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,
@@ -29,6 +30,8 @@ const SELECTORS = {
29
30
  effortMenuItem: 'button, [role="menuitem"], [role="menuitemradio"]',
30
31
  effortMenuLabel: '.__menu-label, [class*="menu-label"]',
31
32
  effortSubmenuTrigger: '[role="menuitem"][aria-haspopup="menu"], button[aria-haspopup="menu"]',
33
+ toolsButton:
34
+ 'button[data-testid="composer-plus-btn"], button[aria-label="Add files and more"]',
32
35
  selectedMenuIndicator:
33
36
  '[aria-checked="true"], [aria-selected="true"], [data-selected="true"], [data-state="checked"], [data-state="selected"], [data-state="on"]',
34
37
  assistantMessage:
@@ -170,257 +173,679 @@ async function waitForPromptReady(cdp, timeoutMs = 30000, signal) {
170
173
  return false;
171
174
  }
172
175
 
173
- function verificationError(kind, requested, items = [], invalid = false) {
174
- const safeRequested = String(requested || "").replace(/\s+/g, " ").trim().slice(0, 80);
175
- const available = boundedOptionLabels(items);
176
- const accepted = kind === "effort" ? ` Accepted: ${CHATGPT_EFFORT_CHOICES.join(", ")}.` : "";
177
- const availableMessage = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
178
- const error = new Error(
179
- invalid
180
- ? `Invalid ChatGPT effort "${safeRequested}".${accepted}`
181
- : `ChatGPT ${kind} verification failed for "${safeRequested}".${accepted}${availableMessage}`,
182
- );
183
- error.code = "model_verification_failed";
176
+ function uiError(code, message) {
177
+ const error = new Error(message);
178
+ error.code = code;
184
179
  return error;
185
180
  }
186
181
 
187
- async function readPicker(cdp, kind, click = false) {
188
- const selector = kind === "model" ? SELECTORS.modelButton : SELECTORS.effortButton;
182
+ async function readChatTabState(cdp, signal) {
189
183
  return evaluate(
190
184
  cdp,
191
185
  `(() => {
192
- ${buildClickDispatcher()}
193
- const kind = ${JSON.stringify(kind)};
194
- const effortChoices = new Set(${JSON.stringify(CHATGPT_EFFORT_CHOICES)});
195
- const effortOwnerLabels = new Set([...effortChoices, 'thinking']);
196
- const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
197
- const labelFor = (node) => {
198
- const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
199
- .split(/\s+/)
200
- .map((id) => document.getElementById(id)?.textContent || '')
201
- .join(' ');
202
- const text = (node.innerText || node.textContent || '').replace(/\s+/g, ' ').trim();
203
- const aria = (node.getAttribute?.('aria-label') || '').replace(/\s+/g, ' ').trim();
204
- const title = (node.getAttribute?.('title') || '').replace(/\s+/g, ' ').trim();
205
- return [text, aria, labelledBy, title].filter(Boolean).join(' | ');
186
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
187
+ const visible = (node) => {
188
+ if (!node || node.hasAttribute?.('hidden') || node.getAttribute?.('aria-hidden') === 'true') return false;
189
+ const style = window.getComputedStyle?.(node);
190
+ if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
191
+ const rect = node.getBoundingClientRect?.();
192
+ return !rect || (rect.width > 0 && rect.height > 0);
206
193
  };
207
- let nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
208
- const value = normalize(labelFor(node));
209
- if (kind !== 'model') {
210
- const words = value.split(/\s+/).filter(Boolean);
211
- const hasEffort = words.some((word) => effortOwnerLabels.has(word));
212
- const looksLikeModel = node.getAttribute?.('data-testid') === 'model-switcher-dropdown-button' ||
213
- value.includes('current model') || value.includes('gpt') || value.includes('instant');
214
- return hasEffort && !looksLikeModel;
215
- }
216
- return value.includes('gpt') || value.includes('thinking') || value.includes('instant');
194
+ const selected = (node) => node.getAttribute?.('aria-selected') === 'true' ||
195
+ node.getAttribute?.('aria-current') === 'page' ||
196
+ node.getAttribute?.('data-state') === 'active' ||
197
+ node.getAttribute?.('data-state') === 'selected' ||
198
+ node.getAttribute?.('data-active') === 'true' ||
199
+ /\\b(active|selected)\\b/i.test(String(node.className || ''));
200
+ const labelFor = (node) => [
201
+ node.innerText || node.textContent || '',
202
+ node.getAttribute?.('aria-label') || '',
203
+ node.getAttribute?.('title') || '',
204
+ ].join(' ').replace(/\\s+/g, ' ').trim();
205
+ const matchesName = (node, name) => [
206
+ node.innerText || node.textContent || '',
207
+ node.getAttribute?.('aria-label') || '',
208
+ node.getAttribute?.('title') || '',
209
+ ].some((value) => {
210
+ const normalized = normalize(value);
211
+ return normalized === name || normalized === name + ' tab' || normalized === 'switch to ' + name;
217
212
  });
218
- if (kind === 'model' && nodes.length === 0) {
219
- nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) => {
220
- const value = normalize(labelFor(node));
221
- return value.includes('pro');
222
- });
223
- }
224
- if (kind === 'model' && nodes.length === 0) {
225
- nodes = Array.from(document.querySelectorAll(${JSON.stringify(selector)})).filter((node) =>
226
- node.getAttribute?.('aria-haspopup') === 'menu' || node.getAttribute?.('aria-expanded') !== null
227
- );
228
- }
229
- const items = nodes.map((node) => {
230
- const text = (node.innerText || node.textContent || '').replace(/\\s+/g, ' ').trim();
231
- const aria = (node.getAttribute?.('aria-label') || '').replace(/\\s+/g, ' ').trim();
232
- const labelledBy = String(node.getAttribute?.('aria-labelledby') || '')
233
- .split(/\\s+/)
234
- .map((id) => document.getElementById(id)?.textContent || '')
235
- .join(' ')
236
- .replace(/\\s+/g, ' ')
237
- .trim();
238
- const title = (node.getAttribute?.('title') || '').replace(/\\s+/g, ' ').trim();
239
- return {
240
- role: node.getAttribute?.('role') || (node.tagName === 'BUTTON' ? 'button' : null),
241
- label: [text, aria, labelledBy, title].filter(Boolean).join(' | ').slice(0, 240),
242
- displayLabel: (text || aria || labelledBy || title).slice(0, 80),
243
- testId: node.getAttribute?.('data-testid') || null,
244
- };
213
+ const nodes = Array.from(new Set([
214
+ ...document.querySelectorAll('[role="tab"], button, a'),
215
+ ])).filter(visible);
216
+ const details = (node) => ({ label: labelFor(node).slice(0, 120), selected: selected(node) });
217
+ const matches = (name) => nodes
218
+ .filter((node) => matchesName(node, name))
219
+ .map(details);
220
+ return { chat: matches('chat'), work: matches('work') };
221
+ })()`,
222
+ signal,
223
+ );
224
+ }
225
+
226
+ async function selectChatTab(cdp, timeoutMs = 8000, signal) {
227
+ throwIfAborted(signal);
228
+ const read = () => readChatTabState(cdp, signal);
229
+ let state = await read();
230
+ if (state?.chat?.length === 1 && state.chat[0].selected) return state.chat[0].label;
231
+ if (state?.chat?.length === 0 && state?.work?.length > 0) {
232
+ throw uiError(
233
+ "chat_mode_unavailable",
234
+ "ChatGPT only exposes Work mode; the Chat tab is required for Oracle",
235
+ );
236
+ }
237
+ if (state?.chat?.length !== 1) {
238
+ throw uiError(
239
+ "chat_mode_selector_drift",
240
+ state?.chat?.length
241
+ ? "ChatGPT Chat tab selector drift: Chat tab is ambiguous"
242
+ : "ChatGPT Chat tab selector drift: Chat tab was not found",
243
+ );
244
+ }
245
+
246
+ const clicked = await evaluate(
247
+ cdp,
248
+ `(() => {
249
+ ${buildClickDispatcher()}
250
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
251
+ const matchesName = (node) => [
252
+ node.innerText || node.textContent || '',
253
+ node.getAttribute?.('aria-label') || '',
254
+ node.getAttribute?.('title') || '',
255
+ ].some((value) => {
256
+ const normalized = normalize(value);
257
+ return normalized === 'chat' || normalized === 'chat tab' || normalized === 'switch to chat';
245
258
  });
246
- if (${click} && nodes.length === 1) dispatchClickSequence(nodes[0]);
247
- return { items };
259
+ const nodes = Array.from(new Set([
260
+ ...document.querySelectorAll('[role="tab"], button, a'),
261
+ ])).filter(matchesName);
262
+ return nodes.length === 1 ? dispatchClickSequence(nodes[0]) : false;
263
+ })()`,
264
+ signal,
265
+ );
266
+ if (!clicked) {
267
+ throw uiError("chat_mode_selector_drift", "ChatGPT Chat tab selector drift: Chat tab could not be clicked");
268
+ }
269
+
270
+ const deadline = Date.now() + timeoutMs;
271
+ while (Date.now() < deadline) {
272
+ await delay(100, signal);
273
+ state = await read();
274
+ if (state?.chat?.length === 1 && state.chat[0].selected) return state.chat[0].label;
275
+ if (state?.chat?.length === 0 && state?.work?.length > 0) {
276
+ throw uiError(
277
+ "chat_mode_unavailable",
278
+ "ChatGPT only exposes Work mode; the Chat tab is required for Oracle",
279
+ );
280
+ }
281
+ }
282
+ throw uiError("chat_mode_selection_failed", "ChatGPT Chat tab could not be selected before timeout");
283
+ }
284
+
285
+ async function readGitHubToolState(cdp, signal) {
286
+ return evaluate(
287
+ cdp,
288
+ `(() => {
289
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
290
+ const visible = (node) => {
291
+ if (!node || node.hasAttribute?.('hidden') || node.getAttribute?.('aria-hidden') === 'true') return false;
292
+ const style = window.getComputedStyle?.(node);
293
+ if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
294
+ const rect = node.getBoundingClientRect?.();
295
+ return !rect || (rect.width > 0 && rect.height > 0);
296
+ };
297
+ const labelFor = (node) => [
298
+ node.innerText || node.textContent || '',
299
+ node.getAttribute?.('aria-label') || '',
300
+ node.getAttribute?.('title') || '',
301
+ node.getAttribute?.('data-testid') || '',
302
+ node.getAttribute?.('data-tool') || '',
303
+ ].join(' ').replace(/\\s+/g, ' ').trim();
304
+ const selected = (node, label) => node.getAttribute?.('aria-checked') === 'true' ||
305
+ node.getAttribute?.('aria-selected') === 'true' ||
306
+ node.getAttribute?.('aria-pressed') === 'true' ||
307
+ node.getAttribute?.('data-selected') === 'true' ||
308
+ node.getAttribute?.('data-active') === 'true' ||
309
+ ['checked', 'selected', 'on', 'active'].includes(normalize(node.getAttribute?.('data-state'))) ||
310
+ /\\b(active|selected)\\b/i.test(label);
311
+ const controls = Array.from(new Set([
312
+ ...document.querySelectorAll('[role="menuitem"], [role="option"], [role="button"], button, [data-testid*="github" i], [data-tool*="github" i], [aria-label*="github" i], [title*="github" i]'),
313
+ ])).filter(visible);
314
+ const github = controls.filter((node) => normalize(labelFor(node)).includes('github'));
315
+ return {
316
+ controls: github.map((node) => {
317
+ const label = labelFor(node).slice(0, 160);
318
+ return {
319
+ label,
320
+ role: node.getAttribute?.('role') || (node.tagName === 'BUTTON' ? 'button' : null),
321
+ selected: selected(node, label),
322
+ disconnected: /\\b(disconnected|not connected|connect|authorize|sign in)\\b/i.test(label),
323
+ testId: node.getAttribute?.('data-testid') || null,
324
+ };
325
+ }),
326
+ };
248
327
  })()`,
328
+ signal,
249
329
  );
250
330
  }
251
331
 
252
- async function readMenu(cdp, kind, allowSubmenu) {
253
- const isModel = kind === "model";
254
- const menuSelector = isModel ? SELECTORS.modelMenu : SELECTORS.effortMenu;
255
- const itemSelector = isModel ? SELECTORS.modelMenuItem : SELECTORS.effortMenuItem;
332
+ async function openToolsMenu(cdp, signal) {
256
333
  return evaluate(
257
334
  cdp,
258
335
  `(() => {
259
336
  ${buildClickDispatcher()}
260
- const isModel = ${isModel};
261
- const choices = ${JSON.stringify(CHATGPT_EFFORT_CHOICES)};
262
- const containers = Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}));
263
- const normalize = (value) => String(value || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
264
- let menu = isModel ? containers.find((container) => {
265
- const labels = Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
266
- .filter((item) => item.getAttribute?.('aria-haspopup') !== 'menu')
267
- .map((item) => normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || '')));
268
- return labels.some((label) => label.includes('gpt') || /^o[0-9]/.test(label));
269
- }) : containers.find((container) => {
270
- const label = normalize(container.querySelector?.(${JSON.stringify(SELECTORS.effortMenuLabel)})?.textContent);
271
- const levels = new Set(Array.from(container.querySelectorAll(${JSON.stringify(itemSelector)}))
272
- .flatMap((item) => normalize(item.textContent).split(/\\s+/))
273
- .filter((word) => choices.includes(word)));
274
- return label.includes('thinking time') || levels.size >= 2;
275
- });
276
- if (!menu && isModel && ${allowSubmenu}) {
277
- for (const container of containers) {
278
- const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
279
- .find((item) => {
280
- const label = normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || ''));
281
- return label.includes('model') || label.includes('advanced');
282
- });
283
- if (trigger) {
284
- dispatchClickSequence(trigger);
285
- return { found: false, submenuOpened: true, items: [] };
286
- }
287
- }
337
+ const selectors = ${JSON.stringify(SELECTORS.toolsButton.split(", "))};
338
+ const visible = (node) => {
339
+ if (!node || node.hasAttribute?.('hidden') || node.getAttribute?.('aria-hidden') === 'true') return false;
340
+ const style = window.getComputedStyle?.(node);
341
+ return !style || (style.display !== 'none' && style.visibility !== 'hidden');
342
+ };
343
+ for (const selector of selectors) {
344
+ const node = Array.from(document.querySelectorAll(selector)).find(visible);
345
+ if (node) return dispatchClickSequence(node);
288
346
  }
289
- if (!menu && !isModel && ${allowSubmenu}) {
290
- for (const container of containers) {
291
- const trigger = Array.from(container.querySelectorAll(${JSON.stringify(SELECTORS.effortSubmenuTrigger)}))
292
- .find((item) => {
293
- const label = normalize((item.getAttribute?.('aria-label') || '') + ' ' + (item.textContent || ''));
294
- return label.includes('thinking time') || label.includes('reasoning effort');
295
- });
296
- if (trigger) {
297
- dispatchClickSequence(trigger);
298
- return { found: false, submenuOpened: true, items: [] };
299
- }
300
- }
347
+ return false;
348
+ })()`,
349
+ signal,
350
+ );
351
+ }
352
+
353
+ async function clickGitHubTool(cdp, signal) {
354
+ return evaluate(
355
+ cdp,
356
+ `(() => {
357
+ ${buildClickDispatcher()}
358
+ const normalize = (value) => String(value || '').replace(/\\s+/g, ' ').trim().toLowerCase();
359
+ const labelFor = (node) => [
360
+ node.innerText || node.textContent || '',
361
+ node.getAttribute?.('aria-label') || '',
362
+ node.getAttribute?.('title') || '',
363
+ node.getAttribute?.('data-testid') || '',
364
+ node.getAttribute?.('data-tool') || '',
365
+ ].join(' ').replace(/\\s+/g, ' ').trim();
366
+ const controls = Array.from(new Set([
367
+ ...document.querySelectorAll('[role="menuitem"], [role="option"], [role="button"], button, [data-testid*="github" i], [data-tool*="github" i], [aria-label*="github" i], [title*="github" i]'),
368
+ ])).filter((node) => normalize(labelFor(node)).includes('github'));
369
+ const menuItems = controls.filter((node) => ['menuitem', 'option'].includes(node.getAttribute?.('role')));
370
+ const candidates = menuItems.length > 0 ? menuItems : controls;
371
+ return candidates.length === 1 ? dispatchClickSequence(candidates[0]) : false;
372
+ })()`,
373
+ signal,
374
+ );
375
+ }
376
+
377
+ async function selectGitHubTool(cdp, timeoutMs = 10000, signal) {
378
+ throwIfAborted(signal);
379
+ let state = await readGitHubToolState(cdp, signal);
380
+ const findConnected = () => state?.controls?.filter((control) => !control.disconnected) || [];
381
+ if (state?.controls?.some((control) => control.disconnected)) {
382
+ throw uiError(
383
+ "github_tool_disconnected",
384
+ "ChatGPT GitHub tool is missing or disconnected; connect GitHub before running Oracle with --github",
385
+ );
386
+ }
387
+ if (findConnected().length === 1 && state.controls[0].selected) return state.controls[0].label;
388
+ if (state?.controls?.length > 1) {
389
+ throw uiError("github_tool_selector_drift", "ChatGPT GitHub tool selector drift: GitHub tool is ambiguous");
390
+ }
391
+ if (state?.controls?.length === 0) {
392
+ const opened = await openToolsMenu(cdp, signal);
393
+ if (!opened) {
394
+ throw uiError("github_tool_selector_drift", "ChatGPT tools selector drift: tools menu could not be opened");
395
+ }
396
+ const menuDeadline = Date.now() + timeoutMs;
397
+ while (Date.now() < menuDeadline) {
398
+ await delay(100, signal);
399
+ state = await readGitHubToolState(cdp, signal);
400
+ if (state?.controls?.some((control) => control.disconnected)) {
401
+ throw uiError(
402
+ "github_tool_disconnected",
403
+ "ChatGPT GitHub tool is missing or disconnected; connect GitHub before running Oracle with --github",
404
+ );
405
+ }
406
+ if (state?.controls?.length > 0) break;
407
+ }
408
+ }
409
+ if (!state?.controls?.length) {
410
+ throw uiError("github_tool_missing", "ChatGPT GitHub tool is not available in the tools menu");
411
+ }
412
+ if (state.controls.length === 1 && state.controls[0].selected) return state.controls[0].label;
413
+ if (state.controls.length !== 1 || !(await clickGitHubTool(cdp, signal))) {
414
+ throw uiError("github_tool_selector_drift", "ChatGPT GitHub tool selector drift: GitHub tool could not be selected");
415
+ }
416
+ const deadline = Date.now() + timeoutMs;
417
+ while (Date.now() < deadline) {
418
+ await delay(100, signal);
419
+ state = await readGitHubToolState(cdp, signal);
420
+ if (state?.controls?.some((control) => control.disconnected)) {
421
+ throw uiError(
422
+ "github_tool_disconnected",
423
+ "ChatGPT GitHub tool became disconnected while selecting it",
424
+ );
425
+ }
426
+ if (state?.controls?.length === 1 && state.controls[0].selected) return state.controls[0].label;
427
+ }
428
+ throw uiError("github_tool_selection_failed", "ChatGPT GitHub tool could not be verified after selection");
429
+ }
430
+
431
+ async function waitForChatGPTAttachment(cdp, filePaths, timeoutMs = 30000, signal) {
432
+ const expectedNames = (Array.isArray(filePaths) ? filePaths : [filePaths])
433
+ .map((filePath) => String(filePath).split(/[\\/]/).pop().toLowerCase());
434
+ const deadline = Date.now() + timeoutMs;
435
+ while (Date.now() < deadline) {
436
+ const state = await evaluate(
437
+ cdp,
438
+ `(() => {
439
+ const scope = document.querySelector('form') || document.querySelector('[data-testid*="composer"]') || document;
440
+ const text = (scope.innerText || scope.textContent || '').replace(/\\s+/g, ' ').trim();
441
+ const inputs = Array.from(scope.querySelectorAll?.('input[type="file"]') || []);
442
+ const fileCount = inputs.reduce((count, input) => count + (input.files?.length || 0), 0);
443
+ const attachmentNodes = Array.from(scope.querySelectorAll?.('[data-testid*="attachment" i], [data-testid*="file" i], [aria-label*="remove attachment" i], [aria-label*="remove file" i]') || [])
444
+ .filter((node) => node.tagName !== 'INPUT' || node.type !== 'file');
445
+ const hasAttachmentNode = attachmentNodes.some((node) => {
446
+ const rect = node.getBoundingClientRect?.();
447
+ return !rect || (rect.width > 0 && rect.height > 0);
448
+ });
449
+ const attachmentLabels = attachmentNodes
450
+ .filter((node) => {
451
+ const rect = node.getBoundingClientRect?.();
452
+ return !rect || (rect.width > 0 && rect.height > 0);
453
+ })
454
+ .map((node) => [
455
+ node.innerText || node.textContent || '',
456
+ node.getAttribute?.('aria-label') || '',
457
+ node.getAttribute?.('title') || '',
458
+ ].join(' ').replace(/\\s+/g, ' ').trim().toLowerCase())
459
+ .filter(Boolean);
460
+ const processingError = /\\b(upload failed|failed to upload|couldn.t upload|unsupported file|file too large|processing failed|error processing)\\b/i.test(text);
461
+ return { fileCount, hasAttachmentNode, attachmentLabels, processingError, text: text.slice(-300) };
462
+ })()`,
463
+ signal,
464
+ );
465
+ if (state?.processingError) {
466
+ throw uiError(
467
+ "attachment_processing",
468
+ `ChatGPT attachment processing failed${state.text ? `: ${state.text}` : ""}`,
469
+ );
470
+ }
471
+ const attachmentLabels = Array.isArray(state?.attachmentLabels) ? state.attachmentLabels : [];
472
+ const hasExpectedName = expectedNames.every((expected) =>
473
+ attachmentLabels.some((label) => label.includes(expected)),
474
+ );
475
+ const hasPotentialFilename = attachmentLabels.some((label) => /(?:^|\\s)[\\w.-]+\\.[a-z0-9]{1,8}(?:$|\\s|[)\\]])/i.test(label));
476
+ if (state?.hasAttachmentNode && (hasExpectedName || !hasPotentialFilename)) return true;
477
+ await delay(250, signal);
478
+ }
479
+ throw uiError(
480
+ "attachment_processing",
481
+ `ChatGPT attachment processing did not complete for ${expectedNames.filter(Boolean).join(", ") || "the selected file"}`,
482
+ );
483
+ }
484
+
485
+ function verificationError(kind, requested, items = [], invalid = false) {
486
+ const safeRequested = String(requested || "").replace(/\s+/g, " ").trim().slice(0, 80);
487
+ const available = boundedOptionLabels(items);
488
+ const accepted = kind === "effort" ? ` Accepted: ${CHATGPT_EFFORT_CHOICES.join(", ")}.` : "";
489
+ const availableMessage = available.length > 0 ? ` Available: ${available.join(", ")}.` : "";
490
+ const error = new Error(
491
+ invalid
492
+ ? `Invalid ChatGPT effort "${safeRequested}".${accepted}`
493
+ : `ChatGPT ${kind} verification failed for "${safeRequested}".${accepted}${availableMessage}`,
494
+ );
495
+ error.code = "model_verification_failed";
496
+ return error;
497
+ }
498
+
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;
301
511
  }
302
- if (!menu) return { found: false, items: [] };
303
- const items = Array.from(menu.querySelectorAll(${JSON.stringify(itemSelector)})).map((item) => {
304
- const primary = isModel ? item.querySelector?.(${JSON.stringify(SELECTORS.menuItemPrimaryLabel)}) : null;
305
- const label = (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '')
306
- .replace(/\\s+/g, ' ').trim().slice(0, 80);
307
- const state = (item.getAttribute?.('data-state') || '').toLowerCase();
308
- const selected = item.getAttribute?.('aria-checked') === 'true' ||
309
- item.getAttribute?.('aria-selected') === 'true' || item.getAttribute?.('data-selected') === 'true' ||
310
- ['checked', 'selected', 'on'].includes(state) ||
311
- Boolean(item.querySelector?.(${JSON.stringify(SELECTORS.selectedMenuIndicator)}));
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();
312
583
  return {
313
- role: item.getAttribute?.('role') || (item.tagName === 'BUTTON' ? 'button' : null),
314
- label,
315
- testId: item.getAttribute?.('data-testid') || null,
316
- selected,
584
+ ...item,
585
+ selected: node.getAttribute?.('aria-checked') === 'true' || state === 'checked',
317
586
  };
318
- });
319
- return { found: true, items };
320
- })()`,
321
- );
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);
322
630
  }
323
631
 
324
- async function waitForMenu(cdp, kind, timeoutMs, signal) {
632
+ async function waitForCombinedMenu(cdp, timeoutMs, signal) {
325
633
  const deadline = Date.now() + timeoutMs;
326
- let allowSubmenu = true;
327
634
  while (Date.now() < deadline) {
328
- const result = await readMenu(cdp, kind, allowSubmenu);
329
- if (result?.found) return result;
330
- if (result?.submenuOpened) allowSubmenu = false;
635
+ const result = await readCombinedPicker(cdp, false, signal);
636
+ if (result?.menuFound) return result;
331
637
  await delay(100, signal);
332
638
  }
333
- return { found: false, items: [] };
639
+ return { menuFound: false, modelItems: [], modelOptions: [], effortItems: [] };
640
+ }
641
+
642
+ async function activateAdvancedModelView(cdp, signal) {
643
+ return evaluate(
644
+ cdp,
645
+ `(() => {
646
+ ${buildClickDispatcher()}
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;
653
+ }
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);
661
+ })()`,
662
+ signal,
663
+ );
334
664
  }
335
665
 
336
- async function clickMenuItem(cdp, kind, match) {
337
- const isModel = kind === "model";
338
- const menuSelector = isModel ? SELECTORS.modelMenu : SELECTORS.effortMenu;
339
- const itemSelector = isModel ? SELECTORS.modelMenuItem : SELECTORS.effortMenuItem;
666
+ async function clickModelOption(cdp, match, signal) {
340
667
  return evaluate(
341
668
  cdp,
342
669
  `(() => {
343
670
  ${buildClickDispatcher()}
344
671
  const expectedTestId = ${JSON.stringify(match.testId)};
345
672
  const expectedLabel = ${JSON.stringify(match.label)};
346
- const items = Array.from(new Set(
347
- Array.from(document.querySelectorAll(${JSON.stringify(menuSelector)}))
348
- .flatMap((menu) => Array.from(menu.querySelectorAll(${JSON.stringify(itemSelector)}))),
349
- ));
350
- 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) => {
351
689
  if (expectedTestId) return item.getAttribute?.('data-testid') === expectedTestId;
352
- const primary = ${isModel} ? item.querySelector?.(${JSON.stringify(SELECTORS.menuItemPrimaryLabel)}) : null;
353
- const label = (primary?.textContent || item.getAttribute?.('aria-label') || item.textContent || '')
354
- .replace(/\\s+/g, ' ').trim().slice(0, 80);
690
+ const label = normalizeText(item.innerText || item.textContent || item.getAttribute?.('aria-label') || '').slice(0, 240);
355
691
  return label === expectedLabel;
356
692
  });
357
- return matches.length >= 1 ? dispatchClickSequence(matches[0]) : false;
693
+ return matches.length === 1 ? dispatchClickSequence(matches[0]) : false;
358
694
  })()`,
695
+ signal,
359
696
  );
360
697
  }
361
698
 
362
- 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) {
363
717
  throwIfAborted(signal);
364
- const picker = await readPicker(cdp, "model", true);
365
- if (picker?.items?.length !== 1) throw verificationError("model", desiredModel);
366
- await delay(300, signal);
367
- const menu = await waitForMenu(cdp, "model", timeoutMs, signal);
368
- const currentAdvancedModel = verifyChatGPTModelSelection(
369
- menu.items.filter((item) => /\bmodel\b/i.test(String(item?.label || ""))),
370
- desiredModel,
371
- );
372
- 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
+ }
373
742
 
374
- const match = resolveChatGPTModelMenuOption(menu.items, desiredModel);
375
- if (!match || !(await clickMenuItem(cdp, "model", match))) {
376
- throw verificationError("model", desiredModel, menu.items);
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
+ }
754
+
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);
377
766
  }
378
767
  await delay(200, signal);
379
- const state = await readPicker(cdp, "model");
380
- const verified = verifyChatGPTModelSelection(state?.items, desiredModel);
381
- if (verified) return verified.displayLabel || verified.label;
382
-
383
- const reopened = await readPicker(cdp, "model", true);
384
- if (reopened?.items?.length !== 1) throw verificationError("model", desiredModel, menu.items);
385
- const readbackMenu = await waitForMenu(cdp, "model", timeoutMs, signal);
386
- const readbackVerified = verifyChatGPTModelSelection(
387
- readbackMenu.items.filter((item) => item.selected),
388
- 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,
389
791
  );
390
- if (!readbackVerified) throw verificationError("model", desiredModel, readbackMenu.items);
391
- return readbackVerified.label;
392
792
  }
393
793
 
394
- 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) {
395
817
  throwIfAborted(signal);
396
818
  const normalizedEffort = normalizeChatGPTEffortChoice(desiredEffort);
397
819
  if (!normalizedEffort) throw verificationError("effort", desiredEffort, [], true);
398
- const current = await readPicker(cdp, "effort");
399
- const currentVerified = verifyChatGPTEffortSelection(current?.items, normalizedEffort);
400
- if (currentVerified) return currentVerified.displayLabel || currentVerified.label;
820
+ const targetValue = CHATGPT_EFFORT_VALUE.get(normalizedEffort);
401
821
 
402
- const picker = await readPicker(cdp, "effort", true);
403
- if (picker?.items?.length !== 1) throw verificationError("effort", desiredEffort);
404
- await delay(300, signal);
405
- const menu = await waitForMenu(cdp, "effort", timeoutMs, signal);
406
- const match = resolveChatGPTEffortMenuOption(menu.items, normalizedEffort);
407
- if (!match || !(await clickMenuItem(cdp, "effort", match))) {
408
- throw verificationError("effort", desiredEffort, menu.items);
409
- }
822
+ const picker = await readCombinedPicker(cdp, true, signal);
823
+ if (picker?.pickerButtons?.length !== 1) throw verificationError("effort", desiredEffort);
410
824
  await delay(200, signal);
411
- const pillState = await readPicker(cdp, "effort");
412
- const pillVerified = verifyChatGPTEffortSelection(pillState?.items, normalizedEffort);
413
- if (pillVerified) return pillVerified.displayLabel || pillVerified.label;
414
-
415
- const reopened = await readPicker(cdp, "effort", true);
416
- if (reopened?.items?.length !== 1) throw verificationError("effort", desiredEffort, menu.items);
417
- const readbackMenu = await waitForMenu(cdp, "effort", timeoutMs, signal);
418
- const verified = verifyChatGPTEffortSelection(
419
- readbackMenu.items.filter((item) => item.selected),
420
- normalizedEffort,
421
- );
422
- if (!verified) throw verificationError("effort", desiredEffort, readbackMenu.items);
423
- 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;
424
849
  }
425
850
 
426
851
  async function typePrompt(cdp, inputCdp, prompt, signal) {
@@ -552,10 +977,14 @@ module.exports = {
552
977
  normalizeChatGPTModelChoice,
553
978
  resolveChatGPTEffortMenuOption,
554
979
  resolveChatGPTModelMenuOption,
980
+ selectChatTab,
555
981
  selectEffort,
982
+ selectGitHubTool,
556
983
  selectModel,
984
+ verifyCurrentModel,
557
985
  verifyChatGPTEffortSelection,
558
986
  verifyChatGPTModelSelection,
987
+ waitForChatGPTAttachment,
559
988
  typePrompt,
560
989
  waitForPageLoad,
561
990
  waitForPromptReady,