ask-pro 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/.codex-plugin/plugin.json +30 -0
  2. package/LICENSE +21 -0
  3. package/README.md +231 -0
  4. package/assets/ask-pro_logo.png +0 -0
  5. package/dist/bin/ask-pro-cli.js +507 -0
  6. package/dist/scripts/run-cli.js +27 -0
  7. package/dist/src/ask-pro/atomicWrite.js +26 -0
  8. package/dist/src/ask-pro/browserRunner.js +796 -0
  9. package/dist/src/ask-pro/responseZip.js +349 -0
  10. package/dist/src/ask-pro/session.js +662 -0
  11. package/dist/src/ask-pro/sessionControllerLease.js +64 -0
  12. package/dist/src/ask-pro/toon.js +26 -0
  13. package/dist/src/ask-pro/zip.js +85 -0
  14. package/dist/src/browser/actions/assistantResponse.js +1245 -0
  15. package/dist/src/browser/actions/attachmentDataTransfer.js +140 -0
  16. package/dist/src/browser/actions/attachments.js +1720 -0
  17. package/dist/src/browser/actions/composerSendReadiness.js +369 -0
  18. package/dist/src/browser/actions/domEvents.js +31 -0
  19. package/dist/src/browser/actions/inputGuard.js +52 -0
  20. package/dist/src/browser/actions/modelPickerDom.js +68 -0
  21. package/dist/src/browser/actions/modelSelection.js +576 -0
  22. package/dist/src/browser/actions/navigation.js +510 -0
  23. package/dist/src/browser/actions/promptComposer.js +824 -0
  24. package/dist/src/browser/actions/remoteFileTransfer.js +37 -0
  25. package/dist/src/browser/actions/thinkingStatus.js +408 -0
  26. package/dist/src/browser/actions/thinkingTime.js +635 -0
  27. package/dist/src/browser/actions/windowState.js +47 -0
  28. package/dist/src/browser/attachRunning.js +31 -0
  29. package/dist/src/browser/chatgptModelCatalog.js +321 -0
  30. package/dist/src/browser/chromeLifecycle.js +807 -0
  31. package/dist/src/browser/config.js +110 -0
  32. package/dist/src/browser/constants.js +85 -0
  33. package/dist/src/browser/cookies.js +191 -0
  34. package/dist/src/browser/detect.js +337 -0
  35. package/dist/src/browser/domDebug.js +72 -0
  36. package/dist/src/browser/errors.js +20 -0
  37. package/dist/src/browser/format.js +16 -0
  38. package/dist/src/browser/index.js +2631 -0
  39. package/dist/src/browser/language.js +97 -0
  40. package/dist/src/browser/liveTabs.js +434 -0
  41. package/dist/src/browser/modelStrategy.js +13 -0
  42. package/dist/src/browser/pageActions.js +5 -0
  43. package/dist/src/browser/profilePaths.js +282 -0
  44. package/dist/src/browser/profileState.js +413 -0
  45. package/dist/src/browser/providerDomFlow.js +17 -0
  46. package/dist/src/browser/providers/chatgptDomProvider.js +50 -0
  47. package/dist/src/browser/reattach.js +534 -0
  48. package/dist/src/browser/reattachHelpers.js +387 -0
  49. package/dist/src/browser/utils.js +122 -0
  50. package/dist/src/browserMode.js +1 -0
  51. package/dist/src/version.js +39 -0
  52. package/package.json +114 -0
  53. package/scripts/refresh-local-plugin.mjs +179 -0
  54. package/scripts/refresh-local-plugin.ps1 +93 -0
  55. package/skills/ask-pro/SKILL.md +181 -0
@@ -0,0 +1,635 @@
1
+ import { INPUT_SELECTORS, MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } from "../constants.js";
2
+ import { logDomFailure } from "../domDebug.js";
3
+ import { buildClickDispatcher } from "./domEvents.js";
4
+ import { buildModelPickerDomHelpers } from "./modelPickerDom.js";
5
+ /**
6
+ * Selects a specific thinking time level in ChatGPT's composer.
7
+ *
8
+ * Best-effort: if the chip / menu / option is missing (for example because
9
+ * ChatGPT moved the effort selector into the per-model trailing button), log a
10
+ * debug dump and continue with whatever effort the UI defaults to.
11
+ *
12
+ * @param level - The intelligence or legacy thinking-time level to select.
13
+ */
14
+ export async function ensureThinkingTime(Runtime, level, logger) {
15
+ const result = await evaluateThinkingTimeSelection(Runtime, level);
16
+ const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
17
+ switch (result?.status) {
18
+ case "already-selected":
19
+ logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
20
+ return;
21
+ case "switched":
22
+ logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
23
+ return;
24
+ case "chip-not-found":
25
+ case "menu-not-found":
26
+ case "option-not-found": {
27
+ await logDomFailure(Runtime, logger, `thinking-${result.status}`);
28
+ if (level === "pro") {
29
+ throw new Error(`Unable to select Pro intelligence: ${result.status.replaceAll("-", " ")}.`);
30
+ }
31
+ logger(`Thinking time: ${result.status.replaceAll("-", " ")} (requested ${capitalizedLevel}); continuing with ChatGPT default.`);
32
+ return;
33
+ }
34
+ default: {
35
+ await logDomFailure(Runtime, logger, "thinking-time-unknown");
36
+ if (level === "pro") {
37
+ throw new Error("Unable to select Pro intelligence: unknown picker outcome.");
38
+ }
39
+ logger(`Thinking time: unknown outcome selecting ${capitalizedLevel}; continuing with ChatGPT default.`);
40
+ return;
41
+ }
42
+ }
43
+ }
44
+ /**
45
+ * Best-effort selection of a thinking time level in ChatGPT's composer pill menu.
46
+ * Safe by default: if the pill/menu/option isn't present, we continue without throwing.
47
+ * @param level - The intelligence or legacy thinking-time level to select.
48
+ */
49
+ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger) {
50
+ try {
51
+ const result = await evaluateThinkingTimeSelection(Runtime, level);
52
+ const capitalizedLevel = level.charAt(0).toUpperCase() + level.slice(1);
53
+ switch (result?.status) {
54
+ case "already-selected":
55
+ logger(`Thinking time: ${result.label ?? capitalizedLevel} (already selected)`);
56
+ return true;
57
+ case "switched":
58
+ logger(`Thinking time: ${result.label ?? capitalizedLevel}`);
59
+ return true;
60
+ case "chip-not-found":
61
+ case "menu-not-found":
62
+ case "option-not-found":
63
+ if (logger.verbose) {
64
+ logger(`Thinking time: ${result.status.replaceAll("-", " ")}; continuing with default.`);
65
+ }
66
+ return false;
67
+ default:
68
+ if (logger.verbose) {
69
+ logger("Thinking time: unknown outcome; continuing with default.");
70
+ }
71
+ return false;
72
+ }
73
+ }
74
+ catch (error) {
75
+ const message = error instanceof Error ? error.message : String(error);
76
+ if (logger.verbose) {
77
+ logger(`Thinking time selection failed (${message}); continuing with default.`);
78
+ await logDomFailure(Runtime, logger, "thinking-time");
79
+ }
80
+ return false;
81
+ }
82
+ }
83
+ async function evaluateThinkingTimeSelection(Runtime, level) {
84
+ const outcome = await Runtime.evaluate({
85
+ expression: buildThinkingTimeExpression(level),
86
+ awaitPromise: true,
87
+ returnByValue: true,
88
+ });
89
+ return outcome.result?.value;
90
+ }
91
+ function buildThinkingTimeExpression(level) {
92
+ const menuContainerLiteral = JSON.stringify(MENU_CONTAINER_SELECTOR);
93
+ const menuItemLiteral = JSON.stringify(MENU_ITEM_SELECTOR);
94
+ const modelButtonLiteral = JSON.stringify(MODEL_BUTTON_SELECTOR);
95
+ const inputSelectorsLiteral = JSON.stringify(INPUT_SELECTORS);
96
+ const targetLevelLiteral = JSON.stringify(level.toLowerCase());
97
+ return `(async () => {
98
+ ${buildClickDispatcher()}
99
+
100
+ const MENU_CONTAINER_SELECTOR = ${menuContainerLiteral};
101
+ const MENU_ITEM_SELECTOR = ${menuItemLiteral};
102
+ const MODEL_BUTTON_SELECTOR = ${modelButtonLiteral};
103
+ const INPUT_SELECTORS = ${inputSelectorsLiteral};
104
+ const TARGET_LEVEL = ${targetLevelLiteral};
105
+
106
+ // English level tokens plus observed localized variants.
107
+ const LEVEL_TOKENS = {
108
+ light: ['light', '轻'],
109
+ standard: ['standard', '标准'],
110
+ extended: ['extended', 'langer', '扩展', '深度', '加强'],
111
+ heavy: ['heavy', '重度', '加重', '高'],
112
+ pro: ['pro'],
113
+ };
114
+ const CURRENT_INTELLIGENCE_TOKENS = {
115
+ light: ['instant', ...LEVEL_TOKENS.light],
116
+ standard: ['medium', ...LEVEL_TOKENS.standard],
117
+ extended: ['pro', ...LEVEL_TOKENS.extended],
118
+ heavy: ['high', ...LEVEL_TOKENS.heavy],
119
+ pro: ['pro'],
120
+ };
121
+ const targetTokens = LEVEL_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
122
+ const currentIntelligenceTokens = CURRENT_INTELLIGENCE_TOKENS[TARGET_LEVEL] || [TARGET_LEVEL];
123
+
124
+ const INITIAL_WAIT_MS = 150;
125
+ const STEP_WAIT_MS = 200;
126
+ const MAX_WAIT_MS = 8000;
127
+
128
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
129
+ const normalize = (value) => (value || '')
130
+ .toLowerCase()
131
+ .normalize('NFD')
132
+ .replace(/[\\u0300-\\u036f]/g, '')
133
+ .replace(/[^a-z0-9\\u4e00-\\u9fa5]+/g, ' ')
134
+ .replace(/\\s+/g, ' ')
135
+ .trim();
136
+ const matchesLevel = (text) => {
137
+ const t = normalize(text);
138
+ return targetTokens.some((tok) => t.includes(String(tok).toLowerCase()));
139
+ };
140
+ const matchesCurrentIntelligenceLevel = (text) => {
141
+ const t = normalize(text);
142
+ return currentIntelligenceTokens.some((tok) => t.startsWith(String(tok).toLowerCase()));
143
+ };
144
+ ${buildModelPickerDomHelpers()}
145
+ const readComposerValue = (node) => {
146
+ if (!node) return '';
147
+ if (typeof HTMLTextAreaElement !== 'undefined' && node instanceof HTMLTextAreaElement) {
148
+ return node.value ?? '';
149
+ }
150
+ return node.innerText ?? node.textContent ?? '';
151
+ };
152
+ const writeComposerValue = (node, value, inputType, data) => {
153
+ if (!node) return;
154
+ if (typeof HTMLTextAreaElement !== 'undefined' && node instanceof HTMLTextAreaElement) {
155
+ node.value = value;
156
+ } else {
157
+ node.textContent = value;
158
+ }
159
+ node.dispatchEvent(new InputEvent('input', { bubbles: true, inputType, data }));
160
+ node.dispatchEvent(new Event('change', { bubbles: true }));
161
+ };
162
+ const findComposerInput = () => {
163
+ const candidates = INPUT_SELECTORS
164
+ .map((selector) => document.querySelector(selector))
165
+ .filter(Boolean);
166
+ return candidates.find((node) => {
167
+ if (!(node instanceof HTMLElement)) return false;
168
+ const rect = node.getBoundingClientRect();
169
+ return rect.width > 0 && rect.height > 0;
170
+ }) || candidates[0] || null;
171
+ };
172
+ let wakeRestore = null;
173
+ const restoreWakeDraft = async () => {
174
+ if (!wakeRestore) return;
175
+ const restore = wakeRestore;
176
+ wakeRestore = null;
177
+ await restore().catch(() => undefined);
178
+ };
179
+ const wakeHiddenModelButton = async () => {
180
+ if (findModelButton()) return;
181
+ const input = findComposerInput();
182
+ if (!(input instanceof HTMLElement)) return;
183
+ dispatchClickSequence(input);
184
+ input.focus?.();
185
+ const before = readComposerValue(input);
186
+ if (before.trim()) {
187
+ await sleep(250);
188
+ return;
189
+ }
190
+ const draft = 'ask-pro model selection';
191
+ writeComposerValue(input, draft, 'insertText', draft);
192
+ await sleep(500);
193
+ wakeRestore = async () => {
194
+ writeComposerValue(input, before, 'deleteByCut', null);
195
+ await sleep(150);
196
+ };
197
+ };
198
+ const optionIsSelected = (node) => {
199
+ if (!(node instanceof HTMLElement)) return false;
200
+ const ariaChecked = node.getAttribute('aria-checked');
201
+ const dataState = (node.getAttribute('data-state') || '').toLowerCase();
202
+ return ariaChecked === 'true' || dataState === 'checked' || dataState === 'selected' || dataState === 'on';
203
+ };
204
+ const closeOpenMenus = () => {
205
+ try {
206
+ document.dispatchEvent(
207
+ new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', keyCode: 27, which: 27, bubbles: true }),
208
+ );
209
+ } catch {}
210
+ };
211
+
212
+ const OLD_CHIP_SELECTORS = [
213
+ '[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
214
+ 'button.__composer-pill[aria-haspopup="menu"]',
215
+ '.__composer-pill-composite button[aria-haspopup="menu"]',
216
+ ];
217
+ const findOldChip = () => {
218
+ for (const selector of OLD_CHIP_SELECTORS) {
219
+ for (const btn of document.querySelectorAll(selector)) {
220
+ if (btn.getAttribute?.('aria-haspopup') !== 'menu') continue;
221
+ const testId = btn.getAttribute?.('data-testid') ?? '';
222
+ if (testId.includes('model-switcher')) continue;
223
+ const aria = normalize(btn.getAttribute?.('aria-label') ?? '');
224
+ const text = normalize(btn.textContent ?? '');
225
+ const label = [aria, text].filter(Boolean).join(' ');
226
+ const hasVersion = /\\b5\\b/.test(label) || /\\b5\\s+[0-9]\\b/.test(label);
227
+ if (!hasVersion && EFFORT_LABELS.has(text)) return btn;
228
+ if (!hasVersion && (aria.includes('thinking') || text.includes('thinking'))) return btn;
229
+ if (!hasVersion && (aria === 'pro' || text === 'pro')) return btn;
230
+ }
231
+ }
232
+ return null;
233
+ };
234
+ const findOldEffortMenu = () => {
235
+ const menus = document.querySelectorAll(
236
+ MENU_CONTAINER_SELECTOR + ', [role="listbox"], [role="group"]'
237
+ );
238
+ for (const menu of menus) {
239
+ const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
240
+ if (normalize(label?.textContent ?? '').includes('thinking time')) return menu;
241
+ const text = normalize(menu.textContent ?? '');
242
+ if (text.includes('standard') && text.includes('extended')) return menu;
243
+ }
244
+ return null;
245
+ };
246
+ const levelOptionScore = (node, options = {}) => {
247
+ const text = normalize(node.textContent ?? '');
248
+ const aria = normalize(node.getAttribute?.('aria-label') ?? '');
249
+ const label = [text, aria].filter(Boolean).join(' ');
250
+ const currentIntelligence = options.currentIntelligence === true;
251
+ if (currentIntelligence) {
252
+ if (!matchesCurrentIntelligenceLevel(label)) return 0;
253
+ let score = 500;
254
+ const role = node.getAttribute?.('role') ?? '';
255
+ if (TARGET_LEVEL === 'extended' && label.includes('5 min')) score += 100;
256
+ if (['menuitemradio', 'option', 'radio'].includes(role)) score += 50;
257
+ return score;
258
+ }
259
+ if (!matchesLevel(label)) return 0;
260
+ if (EFFORT_LABELS.has(text) || EFFORT_LABELS.has(aria)) return 500;
261
+ if (!text.includes('pro') && !text.includes('thinking') && !aria.includes('pro') && !aria.includes('thinking')) {
262
+ return 200;
263
+ }
264
+ return 120;
265
+ };
266
+ const findOptionInMenu = (menu, options = {}) => {
267
+ let best = null;
268
+ for (const item of menu.querySelectorAll(MENU_ITEM_SELECTOR + ', [role="option"]')) {
269
+ const score = levelOptionScore(item, options);
270
+ if (score > 0 && (!best || score > best.score)) {
271
+ best = { item, score };
272
+ }
273
+ }
274
+ return best?.item ?? null;
275
+ };
276
+ const findCurrentIntelligenceMenu = () => {
277
+ const menus = Array.from(document.querySelectorAll(
278
+ MENU_CONTAINER_SELECTOR + ', [role="listbox"], [role="group"]'
279
+ ));
280
+ return menus.find((menu) => {
281
+ const text = normalize(menu.textContent ?? '');
282
+ const testId = normalize(menu.getAttribute?.('data-testid') ?? '');
283
+ return (
284
+ testId.includes('composer intelligence picker content') ||
285
+ (
286
+ text.includes('intelligence') &&
287
+ text.includes('instant') &&
288
+ text.includes('medium') &&
289
+ text.includes('high') &&
290
+ text.includes('pro')
291
+ )
292
+ );
293
+ }) ?? null;
294
+ };
295
+ const selectCurrentIntelligenceLevel = async () => {
296
+ const menu = findCurrentIntelligenceMenu();
297
+ if (!menu) return null;
298
+ const target = findOptionInMenu(menu, { currentIntelligence: true });
299
+ if (!target) {
300
+ const slider = menu.querySelector(
301
+ '[data-model-reasoning-effort-slider] [role="slider"][aria-valuemax]'
302
+ );
303
+ if (TARGET_LEVEL !== 'pro' || !slider) return null;
304
+ const max = Number(slider.getAttribute('aria-valuemax'));
305
+ const before = Number(slider.getAttribute('aria-valuenow'));
306
+ if (!Number.isFinite(max) || !Number.isFinite(before)) return null;
307
+ if (before < max) {
308
+ slider.focus?.();
309
+ slider.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', code: 'End', bubbles: true }));
310
+ slider.dispatchEvent(new KeyboardEvent('keyup', { key: 'End', code: 'End', bubbles: true }));
311
+ await sleep(STEP_WAIT_MS);
312
+ const updated = findCurrentIntelligenceMenu()?.querySelector(
313
+ '[data-model-reasoning-effort-slider] [role="slider"][aria-valuemax]'
314
+ ) ?? slider;
315
+ if (Number(updated.getAttribute('aria-valuenow')) < max) return null;
316
+ }
317
+ return { status: before >= max ? 'already-selected' : 'switched', label: 'Pro' };
318
+ }
319
+ const already = optionIsSelected(target);
320
+ const label = target.textContent?.trim?.() || null;
321
+ if (!already) {
322
+ dispatchClickSequence(target);
323
+ await sleep(STEP_WAIT_MS);
324
+ }
325
+ return { status: already ? 'already-selected' : 'switched', label };
326
+ };
327
+ const finishCurrentIntelligenceLevel = async () => {
328
+ const outcome = await selectCurrentIntelligenceLevel();
329
+ if (!outcome) return null;
330
+ closeOpenMenus();
331
+ await restoreWakeDraft();
332
+ return outcome;
333
+ };
334
+
335
+ await wakeHiddenModelButton();
336
+
337
+ const oldChip = findOldChip();
338
+ if (oldChip) {
339
+ dispatchClickSequence(oldChip);
340
+ const start = performance.now();
341
+ while (performance.now() - start < MAX_WAIT_MS) {
342
+ await sleep(100);
343
+ const newerPickerEvidence = document.querySelectorAll(
344
+ '[data-model-picker-thinking-effort-action="true"], [data-testid="model-configure-modal"], [role="dialog"]',
345
+ );
346
+ if (newerPickerEvidence.length > 0) break;
347
+ const currentIntelligenceOutcome = await finishCurrentIntelligenceLevel();
348
+ if (currentIntelligenceOutcome) {
349
+ return currentIntelligenceOutcome;
350
+ }
351
+ const menu = findOldEffortMenu();
352
+ if (!menu) continue;
353
+ const opt = findOptionInMenu(menu);
354
+ if (!opt) {
355
+ closeOpenMenus();
356
+ await restoreWakeDraft();
357
+ return { status: 'option-not-found' };
358
+ }
359
+ const already = optionIsSelected(opt);
360
+ const label = opt.textContent?.trim?.() || null;
361
+ dispatchClickSequence(opt);
362
+ await sleep(STEP_WAIT_MS);
363
+ closeOpenMenus();
364
+ await restoreWakeDraft();
365
+ return { status: already ? 'already-selected' : 'switched', label };
366
+ }
367
+ closeOpenMenus();
368
+ // Fall through to the newer model-picker effort flow. Some ChatGPT builds
369
+ // expose Pro/Thinking as a composer pill but keep the effort menu under
370
+ // the selected model row.
371
+ }
372
+
373
+ const TRAILING_SELECTOR = '[data-model-picker-thinking-effort-action="true"]';
374
+ const hasTrailingEffortControls = () => document.querySelectorAll(TRAILING_SELECTOR).length > 0;
375
+ const findIntelligenceDialog = () => {
376
+ const dialogs = Array.from(document.querySelectorAll('[role="dialog"], [data-state="open"]'));
377
+ return dialogs.find((dialog) => {
378
+ const text = normalize(dialog.textContent ?? '');
379
+ return (
380
+ (text.includes('model') && text.includes('pro thinking effort')) ||
381
+ (text.includes('modell') && text.includes('denkaufwand pro')) ||
382
+ text.includes('denkaufwand pro')
383
+ );
384
+ }) ?? null;
385
+ };
386
+ const interactiveIn = (root) =>
387
+ Array.from(root.querySelectorAll('button, [role="button"], [role="radio"], [role="combobox"], [role="option"], [role="menuitemradio"], [role="menuitem"], [aria-haspopup]'));
388
+ const findProModelRow = (dialog) => {
389
+ let best = null;
390
+ for (const node of interactiveIn(dialog)) {
391
+ const text = normalize(node.textContent ?? '');
392
+ const aria = normalize(node.getAttribute?.('aria-label') ?? '');
393
+ const testId = (node.getAttribute?.('data-testid') ?? '').toLowerCase();
394
+ const role = node.getAttribute?.('role') ?? '';
395
+ const label = [text, aria, testId].filter(Boolean).join(' ');
396
+ const effortOnly = EFFORT_LABELS.has(text) || EFFORT_LABELS.has(aria);
397
+ const kindFromId = modelKindFromTestId(testId);
398
+ const kindFromText = modelKindFromLabel(label);
399
+ let score = 0;
400
+ if (kindFromId === 'pro') score += 500;
401
+ else if (kindFromId === 'thinking') score -= 500;
402
+ if (kindFromText === 'pro') score += 300;
403
+ else if (kindFromText === 'thinking') score -= 300;
404
+ if (label.includes('pro') && !label.includes('thinking model')) score += 120;
405
+ if (['radio', 'menuitemradio', 'option'].includes(role)) score += 100;
406
+ if (optionIsSelected(node)) score += 80;
407
+ if (effortOnly) score -= 500;
408
+ if (score > 0 && (!best || score > best.score)) best = { node, score };
409
+ }
410
+ return best?.node ?? null;
411
+ };
412
+ const findDialogEffortControl = (dialog, proRow) => {
413
+ let best = null;
414
+ for (const node of interactiveIn(dialog)) {
415
+ if (node === proRow) continue;
416
+ const text = normalize(node.textContent ?? '');
417
+ const aria = normalize(node.getAttribute?.('aria-label') ?? '');
418
+ const title = normalize(node.getAttribute?.('title') ?? '');
419
+ const testId = normalize(node.getAttribute?.('data-testid') ?? '');
420
+ const role = node.getAttribute?.('role') ?? '';
421
+ const label = [text, aria, title, testId].filter(Boolean).join(' ');
422
+ const isEffortValue = EFFORT_LABELS.has(text) || ['standard', 'extended'].includes(text);
423
+ let score = 0;
424
+ if (label.includes('pro thinking effort') || label.includes('denkaufwand pro')) score += 600;
425
+ else if (label.includes('thinking effort') || label.includes('denkaufwand')) score += 400;
426
+ if (role === 'combobox' && (isEffortValue || label.includes('effort') || label.includes('denkaufwand'))) score += 300;
427
+ if ((role === 'button' || node.tagName === 'BUTTON') && (isEffortValue || label.includes('effort') || label.includes('denkaufwand'))) score += 200;
428
+ if (proRow?.contains?.(node)) score += 150;
429
+ if (isEffortValue) score += 100;
430
+ if (score > 0 && (!best || score > best.score)) best = { node, score };
431
+ }
432
+ return best?.node ?? null;
433
+ };
434
+ const findConfigureOption = () =>
435
+ Array.from(document.querySelectorAll('[data-testid="model-configure-modal"], [role="menuitem"], button, [role="button"]'))
436
+ .find((node) => {
437
+ const text = normalize(node.textContent ?? '');
438
+ const testId = normalize(node.getAttribute?.('data-testid') ?? '');
439
+ return testId.includes('model configure modal') || text === 'configure' || text === 'configure...' || text === 'konfigurieren' || text === 'konfigurieren...';
440
+ }) ?? null;
441
+ const findOpenEffortOption = () => {
442
+ const menus = Array.from(document.querySelectorAll(MENU_CONTAINER_SELECTOR + ', [role="listbox"], [role="group"]'));
443
+ let best = null;
444
+ for (const menu of menus) {
445
+ for (const node of menu.querySelectorAll('[role="option"], [role="menuitemradio"], [role="menuitem"], [role="radio"], button')) {
446
+ const score = levelOptionScore(node);
447
+ if (score > 0 && (!best || score > best.score)) best = { node, score };
448
+ }
449
+ }
450
+ if (best) return best.node;
451
+ const candidates = Array.from(document.querySelectorAll('[role="option"], [role="menuitemradio"], [role="menuitem"], [role="radio"], button'));
452
+ for (const node of candidates) {
453
+ const score = levelOptionScore(node);
454
+ if (score > 0 && (!best || score > best.score)) best = { node, score };
455
+ }
456
+ return best?.node ?? null;
457
+ };
458
+
459
+ const modelBtn = findModelButton();
460
+ if (!modelBtn) {
461
+ await restoreWakeDraft();
462
+ return { status: 'chip-not-found' };
463
+ }
464
+
465
+ let intelligenceDialog = findIntelligenceDialog();
466
+ if (!intelligenceDialog) {
467
+ dispatchClickSequence(modelBtn);
468
+ await sleep(INITIAL_WAIT_MS);
469
+ const dialogDeadline = performance.now() + MAX_WAIT_MS;
470
+ while (performance.now() < dialogDeadline) {
471
+ intelligenceDialog = findIntelligenceDialog();
472
+ if (intelligenceDialog) break;
473
+ const configure = findConfigureOption();
474
+ if (configure) {
475
+ dispatchClickSequence(configure);
476
+ await sleep(STEP_WAIT_MS);
477
+ intelligenceDialog = findIntelligenceDialog();
478
+ if (intelligenceDialog) break;
479
+ }
480
+ const currentIntelligenceOutcome = await finishCurrentIntelligenceLevel();
481
+ if (currentIntelligenceOutcome) {
482
+ return currentIntelligenceOutcome;
483
+ }
484
+ if (hasTrailingEffortControls()) break;
485
+ await sleep(100);
486
+ }
487
+ }
488
+
489
+ if (intelligenceDialog) {
490
+ const proRow = findProModelRow(intelligenceDialog);
491
+ if (proRow && !optionIsSelected(proRow)) {
492
+ dispatchClickSequence(proRow);
493
+ await sleep(STEP_WAIT_MS);
494
+ }
495
+ const effortControl = findDialogEffortControl(intelligenceDialog, proRow);
496
+ if (effortControl) {
497
+ const currentLabel = effortControl.textContent?.trim?.() || null;
498
+ if (currentLabel && matchesLevel(currentLabel)) {
499
+ closeOpenMenus();
500
+ await restoreWakeDraft();
501
+ return { status: 'already-selected', label: currentLabel };
502
+ }
503
+ dispatchClickSequence(effortControl);
504
+ await sleep(STEP_WAIT_MS);
505
+ let targetOption = null;
506
+ const optionDeadline = performance.now() + MAX_WAIT_MS;
507
+ while (performance.now() < optionDeadline) {
508
+ targetOption = findOpenEffortOption();
509
+ if (targetOption) break;
510
+ await sleep(100);
511
+ }
512
+ if (!targetOption) {
513
+ closeOpenMenus();
514
+ await restoreWakeDraft();
515
+ return { status: 'option-not-found' };
516
+ }
517
+ const already = optionIsSelected(targetOption);
518
+ const label = targetOption.textContent?.trim?.() || null;
519
+ dispatchClickSequence(targetOption);
520
+ await sleep(STEP_WAIT_MS);
521
+ closeOpenMenus();
522
+ await restoreWakeDraft();
523
+ return { status: already ? 'already-selected' : 'switched', label };
524
+ }
525
+ }
526
+
527
+ const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
528
+ const modelLabel = () => normalize(modelBtn.textContent ?? '');
529
+ const findEffortRow = (trailing) =>
530
+ trailing.closest?.('[class*="model-picker-thinking-effort-row"]') ??
531
+ trailing.closest?.('[data-radix-collection-item]') ??
532
+ trailing.parentElement;
533
+ const pickTrailingForCurrentModel = () => {
534
+ const trailings = findTrailingButtons();
535
+ if (trailings.length === 0) return null;
536
+ const currentLabel = modelLabel();
537
+ const currentKind = modelKindFromLabel(currentLabel);
538
+ let best = null;
539
+ for (const t of trailings) {
540
+ const row = findEffortRow(t);
541
+ const testId = (t.getAttribute?.('data-testid') ?? '').toLowerCase();
542
+ const testIdKind = modelKindFromTestId(testId);
543
+ const rowKind = modelKindFromLabel(row?.textContent ?? '');
544
+ let score = 0;
545
+ const rowSelected = row && (optionIsSelected(row) || row.querySelector('[aria-checked="true"]'));
546
+ if (rowSelected) {
547
+ score += 1000;
548
+ }
549
+ if (currentKind) {
550
+ if (testIdKind === currentKind) score += 500;
551
+ else if (testIdKind) score -= 500;
552
+ if (rowKind === currentKind) score += 250;
553
+ else if (rowKind) score -= 250;
554
+ }
555
+ if (!best || score > best.score) best = { trailing: t, score };
556
+ }
557
+ if (best && best.score > 0) return best.trailing;
558
+ return null;
559
+ };
560
+
561
+ if (modelBtn.getAttribute('aria-expanded') !== 'true') {
562
+ dispatchClickSequence(modelBtn);
563
+ await sleep(INITIAL_WAIT_MS);
564
+ }
565
+
566
+ let trailing = null;
567
+ const trailingDeadline = performance.now() + MAX_WAIT_MS;
568
+ while (performance.now() < trailingDeadline) {
569
+ trailing = pickTrailingForCurrentModel();
570
+ if (trailing) break;
571
+ await sleep(100);
572
+ }
573
+ if (!trailing) {
574
+ closeOpenMenus();
575
+ await restoreWakeDraft();
576
+ return { status: 'option-not-found' };
577
+ }
578
+
579
+ dispatchClickSequence(trailing);
580
+ await sleep(STEP_WAIT_MS);
581
+
582
+ const resolveEffortMenu = () => {
583
+ const id = trailing.getAttribute('aria-controls');
584
+ if (id) {
585
+ const node = document.getElementById(id);
586
+ if (node) return node;
587
+ }
588
+ const menus = document.querySelectorAll(
589
+ MENU_CONTAINER_SELECTOR + ', [role="listbox"], [role="group"]'
590
+ );
591
+ let best = null;
592
+ for (const menu of menus) {
593
+ if (menu === modelBtn || menu.contains(trailing)) continue;
594
+ const text = normalize(menu.textContent ?? '');
595
+ let hits = 0;
596
+ for (const tokens of Object.values(LEVEL_TOKENS)) {
597
+ if (tokens.some((tok) => text.includes(String(tok).toLowerCase()))) hits += 1;
598
+ }
599
+ if (hits >= 2 && (!best || hits > best.hits)) best = { menu, hits };
600
+ }
601
+ return best?.menu ?? null;
602
+ };
603
+
604
+ let effortMenu = null;
605
+ const effortDeadline = performance.now() + MAX_WAIT_MS;
606
+ while (performance.now() < effortDeadline) {
607
+ effortMenu = resolveEffortMenu();
608
+ if (effortMenu) break;
609
+ await sleep(100);
610
+ }
611
+ if (!effortMenu) {
612
+ closeOpenMenus();
613
+ await restoreWakeDraft();
614
+ return { status: 'menu-not-found' };
615
+ }
616
+
617
+ const targetOption = findOptionInMenu(effortMenu);
618
+ if (!targetOption) {
619
+ closeOpenMenus();
620
+ await restoreWakeDraft();
621
+ return { status: 'option-not-found' };
622
+ }
623
+
624
+ const already = optionIsSelected(targetOption);
625
+ const label = targetOption.textContent?.trim?.() || null;
626
+ dispatchClickSequence(targetOption);
627
+ await sleep(STEP_WAIT_MS);
628
+ closeOpenMenus();
629
+ await restoreWakeDraft();
630
+ return { status: already ? 'already-selected' : 'switched', label };
631
+ })()`;
632
+ }
633
+ export function buildThinkingTimeExpressionForTest(level = "extended") {
634
+ return buildThinkingTimeExpression(level);
635
+ }
@@ -0,0 +1,47 @@
1
+ export async function isChromeWindowMinimized(client) {
2
+ const browser = client.Browser;
3
+ if (typeof browser?.getWindowForTarget !== "function")
4
+ return null;
5
+ try {
6
+ const targetId = await readCurrentTargetId(client);
7
+ const { bounds } = await browser.getWindowForTarget(targetId ? { targetId } : undefined);
8
+ return typeof bounds?.windowState === "string" ? bounds.windowState === "minimized" : null;
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ export async function setChromeWindowState(client, windowState, logger, options = {}) {
15
+ const browser = client.Browser;
16
+ if (typeof browser?.getWindowForTarget !== "function" ||
17
+ typeof browser.setWindowBounds !== "function") {
18
+ logger("[browser] Chrome window parking unavailable in this DevTools session.");
19
+ return false;
20
+ }
21
+ try {
22
+ const targetId = options.targetId ?? (await readCurrentTargetId(client));
23
+ const targetParams = targetId ? { targetId } : undefined;
24
+ const { windowId } = await browser.getWindowForTarget(targetParams);
25
+ if (typeof windowId !== "number") {
26
+ logger("[browser] Chrome window parking unavailable: missing window id.");
27
+ return false;
28
+ }
29
+ await browser.setWindowBounds({ windowId, bounds: { windowState } });
30
+ const action = windowState === "minimized" ? "parked (minimized)" : "restored";
31
+ logger(`[browser] Chrome window ${action}${options.reason ? ` (${options.reason})` : ""}`);
32
+ return true;
33
+ }
34
+ catch (error) {
35
+ logger(`[browser] Failed to ${windowState === "minimized" ? "park" : "restore"} Chrome window: ${error instanceof Error ? error.message : String(error)}`);
36
+ return false;
37
+ }
38
+ }
39
+ async function readCurrentTargetId(client) {
40
+ try {
41
+ const info = await client.Target?.getTargetInfo?.({});
42
+ return info?.targetInfo?.targetId;
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }