@steipete/oracle 0.13.0 → 0.14.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.
- package/README.md +20 -9
- package/dist/bin/oracle-cli.js +63 -63
- package/dist/docs-site/.nojekyll +0 -0
- package/dist/docs-site/CNAME +1 -0
- package/dist/docs-site/RELEASING.html +410 -0
- package/dist/docs-site/agents.html +374 -0
- package/dist/docs-site/anthropic.html +368 -0
- package/dist/docs-site/bridge.html +400 -0
- package/dist/docs-site/browser-mode.html +593 -0
- package/dist/docs-site/chromium-forks.html +347 -0
- package/dist/docs-site/cli-reference.html +346 -0
- package/dist/docs-site/configuration.html +452 -0
- package/dist/docs-site/favicon.svg +14 -0
- package/dist/docs-site/followup.html +375 -0
- package/dist/docs-site/gemini.html +383 -0
- package/dist/docs-site/grok.html +325 -0
- package/dist/docs-site/index.html +360 -0
- package/dist/docs-site/install.html +335 -0
- package/dist/docs-site/linux.html +321 -0
- package/dist/docs-site/llms.txt +43 -0
- package/dist/docs-site/manual-tests.html +596 -0
- package/dist/docs-site/mcp.html +391 -0
- package/dist/docs-site/multimodel.html +364 -0
- package/dist/docs-site/mythical-pro-agents.html +360 -0
- package/dist/docs-site/notifier.html +338 -0
- package/dist/docs-site/openai-endpoints.html +387 -0
- package/dist/docs-site/openrouter.html +344 -0
- package/dist/docs-site/quickstart.html +369 -0
- package/dist/docs-site/refactor/ux.html +532 -0
- package/dist/docs-site/sessions.html +388 -0
- package/dist/docs-site/social-card.png +0 -0
- package/dist/docs-site/social-card.svg +79 -0
- package/dist/docs-site/spec.html +363 -0
- package/dist/docs-site/testing.html +320 -0
- package/dist/docs-site/tui-debug.html +326 -0
- package/dist/docs-site/windows-work.html +323 -0
- package/dist/docs-site/windows.html +320 -0
- package/dist/src/browser/actions/assistantResponse.js +91 -9
- package/dist/src/browser/actions/deepResearch.js +180 -52
- package/dist/src/browser/actions/modelSelection.js +13 -11
- package/dist/src/browser/actions/navigation.js +294 -65
- package/dist/src/browser/actions/promptComposer.js +50 -4
- package/dist/src/browser/actions/thinkingTime.js +308 -89
- package/dist/src/browser/artifacts.js +19 -0
- package/dist/src/browser/chatgptFiles.js +797 -0
- package/dist/src/browser/chatgptImages.js +130 -3
- package/dist/src/browser/chromeLifecycle.js +4 -0
- package/dist/src/browser/config.js +2 -0
- package/dist/src/browser/index.js +347 -39
- package/dist/src/browser/pageActions.js +1 -1
- package/dist/src/browser/policies.js +2 -6
- package/dist/src/browser/projectSourcesRunner.js +14 -2
- package/dist/src/browser/prompt.js +11 -15
- package/dist/src/browser/reattach.js +6 -2
- package/dist/src/browser/reattachability.js +22 -10
- package/dist/src/browser/recoverConversation.js +73 -0
- package/dist/src/browser/sessionRunner.js +4 -1
- package/dist/src/cli/browserConfig.js +4 -1
- package/dist/src/cli/browserTabs.js +129 -54
- package/dist/src/cli/followup.js +72 -0
- package/dist/src/cli/help.js +1 -1
- package/dist/src/cli/options.js +16 -0
- package/dist/src/cli/reattachGuidance.js +8 -0
- package/dist/src/cli/runOptions.js +2 -12
- package/dist/src/cli/sessionCommand.js +4 -0
- package/dist/src/cli/sessionDisplay.js +17 -3
- package/dist/src/cli/sessionLineage.js +7 -4
- package/dist/src/cli/sessionRunner.js +16 -1
- package/dist/src/gemini-web/client.js +5 -10
- package/dist/src/gemini-web/executor.js +1 -24
- package/dist/src/gemini-web/models.js +83 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/mcp/tools/chatgptImage.js +127 -0
- package/dist/src/mcp/tools/consult.js +261 -167
- package/dist/src/mcp/types.js +2 -0
- package/dist/src/mcp/utils.js +87 -1
- package/dist/src/oracle/config.js +27 -1
- package/dist/src/oracle/files.js +4 -6
- package/dist/src/oracle/geminiModels.js +2 -0
- package/dist/src/oracle/markdown.js +36 -3
- package/dist/src/oracle/modelResolver.js +21 -13
- package/dist/src/oracle/promptAssembly.js +2 -4
- package/dist/src/oracle/request.js +3 -5
- package/dist/src/oracle/tokenStats.js +5 -1
- package/dist/src/oracle.js +1 -1
- package/dist/src/sessionManager.js +1 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
- package/package.json +19 -28
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
- package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
|
@@ -2,14 +2,25 @@ import { MENU_CONTAINER_SELECTOR, MENU_ITEM_SELECTOR, MODEL_BUTTON_SELECTOR, } f
|
|
|
2
2
|
import { logDomFailure } from "../domDebug.js";
|
|
3
3
|
import { buildClickDispatcher } from "./domEvents.js";
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Surfaces the model-picker snapshot captured alongside a failed detection.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
* The browser prefix routes this through the session runner's non-verbose
|
|
8
|
+
* always-print path. The injected probe bounds and redacts all text values.
|
|
9
|
+
*/
|
|
10
|
+
function logPickerDiagnostic(result, logger) {
|
|
11
|
+
const diagnostic = result && "diagnostic" in result
|
|
12
|
+
? result.diagnostic
|
|
13
|
+
: undefined;
|
|
14
|
+
if (!diagnostic) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
logger(`[browser] Model picker diagnostic: ${JSON.stringify(diagnostic)}`);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Selects a thinking-time level in ChatGPT's composer.
|
|
11
21
|
*
|
|
12
|
-
*
|
|
22
|
+
* Missing controls remain best-effort except Pro Extended, which fails closed
|
|
23
|
+
* unless the selected option is confirmed.
|
|
13
24
|
*/
|
|
14
25
|
export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
15
26
|
const result = await evaluateThinkingTimeSelection(Runtime, level, desiredModel);
|
|
@@ -26,8 +37,10 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
26
37
|
case "chip-not-found":
|
|
27
38
|
case "menu-not-found":
|
|
28
39
|
case "option-not-found":
|
|
40
|
+
case "selection-unverified":
|
|
29
41
|
case "model-kind-not-found": {
|
|
30
42
|
await logDomFailure(Runtime, logger, `thinking-${result.status}`);
|
|
43
|
+
logPickerDiagnostic(result, logger);
|
|
31
44
|
const kindHint = result.status === "model-kind-not-found" && result.modelKind
|
|
32
45
|
? ` for ${result.modelKind}`
|
|
33
46
|
: targetModelKind
|
|
@@ -42,6 +55,7 @@ export async function ensureThinkingTime(Runtime, level, logger, desiredModel) {
|
|
|
42
55
|
}
|
|
43
56
|
default: {
|
|
44
57
|
await logDomFailure(Runtime, logger, "thinking-time-unknown");
|
|
58
|
+
logPickerDiagnostic(result, logger);
|
|
45
59
|
if (strictProEffort) {
|
|
46
60
|
throw new Error(`Thinking time: unknown outcome selecting ${capitalizedLevel}; refusing to submit without confirmed Pro Extended.`);
|
|
47
61
|
}
|
|
@@ -69,6 +83,7 @@ export async function ensureThinkingTimeIfAvailable(Runtime, level, logger, desi
|
|
|
69
83
|
case "chip-not-found":
|
|
70
84
|
case "menu-not-found":
|
|
71
85
|
case "option-not-found":
|
|
86
|
+
case "selection-unverified":
|
|
72
87
|
case "model-kind-not-found":
|
|
73
88
|
if (logger.verbose) {
|
|
74
89
|
logger(`Thinking time: ${result.status.replaceAll("-", " ")}; continuing with default.`);
|
|
@@ -125,6 +140,10 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
125
140
|
const INITIAL_WAIT_MS = 150;
|
|
126
141
|
const STEP_WAIT_MS = 200;
|
|
127
142
|
const MAX_WAIT_MS = 8000;
|
|
143
|
+
// The "Intelligence" menu renders right after opening the composer pill, so
|
|
144
|
+
// a short probe is enough; if it's absent this is an older UI and we fall
|
|
145
|
+
// back to the legacy paths without paying the full MAX_WAIT_MS.
|
|
146
|
+
const INTELLIGENCE_WAIT_MS = 2500;
|
|
128
147
|
|
|
129
148
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
130
149
|
// Keep CJK characters so we can match Chinese labels against LEVEL_TOKENS.
|
|
@@ -141,9 +160,18 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
141
160
|
const optionIsSelected = (node) => {
|
|
142
161
|
if (!(node instanceof HTMLElement)) return false;
|
|
143
162
|
const ariaChecked = node.getAttribute('aria-checked');
|
|
163
|
+
const ariaSelected = node.getAttribute('aria-selected');
|
|
164
|
+
const ariaCurrent = node.getAttribute('aria-current');
|
|
165
|
+
const dataSelected = node.getAttribute('data-selected');
|
|
144
166
|
const dataState = (node.getAttribute('data-state') || '').toLowerCase();
|
|
145
|
-
if (ariaChecked === 'true') return true;
|
|
146
|
-
return
|
|
167
|
+
if (ariaChecked === 'true' || ariaSelected === 'true' || ariaCurrent === 'true') return true;
|
|
168
|
+
return (
|
|
169
|
+
dataSelected === 'true' ||
|
|
170
|
+
dataState === 'checked' ||
|
|
171
|
+
dataState === 'selected' ||
|
|
172
|
+
dataState === 'on' ||
|
|
173
|
+
dataState === 'true'
|
|
174
|
+
);
|
|
147
175
|
};
|
|
148
176
|
const closeOpenMenus = () => {
|
|
149
177
|
try {
|
|
@@ -153,34 +181,100 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
153
181
|
} catch {}
|
|
154
182
|
};
|
|
155
183
|
|
|
156
|
-
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
184
|
+
const TRAILING_SELECTOR = '[data-model-picker-thinking-effort-action="true"]';
|
|
185
|
+
const INTELLIGENCE_MENU_SELECTOR = '[data-testid="composer-intelligence-picker-content"]';
|
|
186
|
+
|
|
187
|
+
const findModelButton = () => document.querySelector(MODEL_BUTTON_SELECTOR);
|
|
188
|
+
const findTrailingButtons = () => Array.from(document.querySelectorAll(TRAILING_SELECTOR));
|
|
189
|
+
const KIND_NOT_FOUND = { kindNotFound: true };
|
|
190
|
+
|
|
191
|
+
const isVisible = (node) => {
|
|
192
|
+
if (!node || node.getAttribute?.('aria-hidden') === 'true') return false;
|
|
193
|
+
const rect = node.getBoundingClientRect?.();
|
|
194
|
+
return Boolean(rect && rect.width > 0 && rect.height > 0);
|
|
195
|
+
};
|
|
196
|
+
const redactDiagnosticText = (value, maxLength = 120) =>
|
|
197
|
+
String(value ?? '')
|
|
198
|
+
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}/gi, '[redacted-email]')
|
|
199
|
+
.replace(/\\b[A-Za-z0-9_-]{32,}\\b/g, '[redacted]')
|
|
200
|
+
.replace(/\\s+/g, ' ')
|
|
201
|
+
.trim()
|
|
202
|
+
.slice(0, maxLength);
|
|
203
|
+
const describeNode = (el) => {
|
|
204
|
+
if (!el || typeof el.getAttribute !== 'function') return null;
|
|
205
|
+
let rect = null;
|
|
206
|
+
try {
|
|
207
|
+
const r = el.getBoundingClientRect?.();
|
|
208
|
+
if (r) {
|
|
209
|
+
rect = {
|
|
210
|
+
w: Math.round(r.width),
|
|
211
|
+
h: Math.round(r.height),
|
|
212
|
+
visible: r.width > 0 && r.height > 0,
|
|
213
|
+
};
|
|
170
214
|
}
|
|
171
|
-
}
|
|
172
|
-
return
|
|
215
|
+
} catch {}
|
|
216
|
+
return {
|
|
217
|
+
tag: el.tagName || null,
|
|
218
|
+
testid: el.getAttribute('data-testid'),
|
|
219
|
+
role: el.getAttribute('role'),
|
|
220
|
+
ariaLabel: redactDiagnosticText(el.getAttribute('aria-label')),
|
|
221
|
+
ariaExpanded: el.getAttribute('aria-expanded'),
|
|
222
|
+
ariaChecked: el.getAttribute('aria-checked'),
|
|
223
|
+
ariaSelected: el.getAttribute('aria-selected'),
|
|
224
|
+
ariaHaspopup: el.getAttribute('aria-haspopup'),
|
|
225
|
+
dataState: el.getAttribute('data-state'),
|
|
226
|
+
text: redactDiagnosticText(el.textContent, 80),
|
|
227
|
+
rect,
|
|
228
|
+
};
|
|
173
229
|
};
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
230
|
+
const describeMenu = (menu) => {
|
|
231
|
+
if (!menu || typeof menu.querySelectorAll !== 'function') return null;
|
|
232
|
+
const items = Array.from(
|
|
233
|
+
menu.querySelectorAll('[role="menuitem"], [role="menuitemradio"], [role="option"], button, [data-testid]'),
|
|
234
|
+
)
|
|
235
|
+
.slice(0, 30)
|
|
236
|
+
.map(describeNode);
|
|
237
|
+
return {
|
|
238
|
+
role: menu.getAttribute?.('role') ?? null,
|
|
239
|
+
testid: menu.getAttribute?.('data-testid') ?? null,
|
|
240
|
+
itemCount: items.length,
|
|
241
|
+
items,
|
|
242
|
+
};
|
|
243
|
+
};
|
|
244
|
+
const collectPickerDiagnostic = () => {
|
|
245
|
+
try {
|
|
246
|
+
const trailings = findTrailingButtons();
|
|
247
|
+
const switchers = Array.from(document.querySelectorAll('[data-testid*="model-switcher"]'));
|
|
248
|
+
const composerButtons = Array.from(
|
|
249
|
+
document.querySelectorAll(
|
|
250
|
+
'form button[aria-haspopup="menu"], [data-testid="model-switcher-dropdown-button"]',
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
const menus = Array.from(document.querySelectorAll(MENU_CONTAINER_SELECTOR)).filter(
|
|
254
|
+
isVisible,
|
|
255
|
+
);
|
|
256
|
+
const modelBtn = findModelButton();
|
|
257
|
+
return {
|
|
258
|
+
targetModelKind: TARGET_MODEL_KIND,
|
|
259
|
+
targetLevel: TARGET_LEVEL,
|
|
260
|
+
modelButton: describeNode(modelBtn),
|
|
261
|
+
composerButtons: composerButtons.slice(0, 12).map(describeNode),
|
|
262
|
+
trailingCount: trailings.length,
|
|
263
|
+
trailings: trailings.slice(0, 12).map(describeNode),
|
|
264
|
+
modelSwitcherCount: switchers.length,
|
|
265
|
+
modelSwitcher: switchers.slice(0, 12).map(describeNode),
|
|
266
|
+
menuCount: menus.length,
|
|
267
|
+
menus: menus.slice(0, 4).map(describeMenu),
|
|
268
|
+
};
|
|
269
|
+
} catch (err) {
|
|
270
|
+
return { error: redactDiagnosticText(err && err.message ? err.message : err) };
|
|
181
271
|
}
|
|
182
|
-
return null;
|
|
183
272
|
};
|
|
273
|
+
const failure = (status, extra = {}) => ({
|
|
274
|
+
status,
|
|
275
|
+
...extra,
|
|
276
|
+
diagnostic: collectPickerDiagnostic(),
|
|
277
|
+
});
|
|
184
278
|
const findOptionInMenu = (menu) => {
|
|
185
279
|
for (const item of menu.querySelectorAll(MENU_ITEM_SELECTOR)) {
|
|
186
280
|
if (
|
|
@@ -192,41 +286,135 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
192
286
|
}
|
|
193
287
|
return null;
|
|
194
288
|
};
|
|
289
|
+
const countEffortLevels = (menu) => {
|
|
290
|
+
const text = normalize(menu?.textContent ?? '');
|
|
291
|
+
let hits = 0;
|
|
292
|
+
for (const tokens of Object.values(LEVEL_TOKENS)) {
|
|
293
|
+
if (tokens.some((token) => text.includes(String(token).toLowerCase()))) hits += 1;
|
|
294
|
+
}
|
|
295
|
+
return hits;
|
|
296
|
+
};
|
|
297
|
+
const isEffortMenu = (menu) => {
|
|
298
|
+
if (!isVisible(menu)) return false;
|
|
299
|
+
const label = menu.querySelector?.('.__menu-label, [class*="menu-label"]');
|
|
300
|
+
const labelText = normalize(label?.textContent ?? '');
|
|
301
|
+
return (
|
|
302
|
+
labelText.includes('thinking time') ||
|
|
303
|
+
labelText.includes('thinking effort') ||
|
|
304
|
+
countEffortLevels(menu) >= 2
|
|
305
|
+
);
|
|
306
|
+
};
|
|
307
|
+
const controlledMenu = (trigger) => {
|
|
308
|
+
const id = trigger?.getAttribute?.('aria-controls');
|
|
309
|
+
if (!id) return null;
|
|
310
|
+
const menu = document.getElementById?.(id);
|
|
311
|
+
return isEffortMenu(menu) ? menu : null;
|
|
312
|
+
};
|
|
313
|
+
const findVisibleEffortMenu = (trigger) => {
|
|
314
|
+
const controlled = controlledMenu(trigger);
|
|
315
|
+
if (controlled) return controlled;
|
|
316
|
+
for (const menu of document.querySelectorAll(MENU_CONTAINER_SELECTOR)) {
|
|
317
|
+
if (isEffortMenu(menu)) return menu;
|
|
318
|
+
}
|
|
319
|
+
return null;
|
|
320
|
+
};
|
|
321
|
+
const selectAndVerify = async (trigger, findOption) => {
|
|
322
|
+
const option = findOption();
|
|
323
|
+
if (!option) return failure('option-not-found');
|
|
324
|
+
const label = option.textContent?.trim?.() || null;
|
|
325
|
+
if (optionIsSelected(option)) {
|
|
326
|
+
closeOpenMenus();
|
|
327
|
+
return { status: 'already-selected', label };
|
|
328
|
+
}
|
|
195
329
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
330
|
+
dispatchClickSequence(option);
|
|
331
|
+
await sleep(STEP_WAIT_MS);
|
|
332
|
+
const refreshed = findOption();
|
|
333
|
+
if (refreshed && optionIsSelected(refreshed)) {
|
|
334
|
+
closeOpenMenus();
|
|
335
|
+
return { status: 'switched', label: refreshed.textContent?.trim?.() || label };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (!refreshed && trigger && trigger.getAttribute?.('aria-expanded') !== 'true') {
|
|
339
|
+
dispatchClickSequence(trigger);
|
|
340
|
+
await sleep(INITIAL_WAIT_MS);
|
|
341
|
+
}
|
|
342
|
+
const deadline = performance.now() + 2000;
|
|
343
|
+
while (performance.now() < deadline) {
|
|
344
|
+
const selected = findOption();
|
|
345
|
+
if (selected && optionIsSelected(selected)) {
|
|
206
346
|
closeOpenMenus();
|
|
207
|
-
return { status: '
|
|
347
|
+
return { status: 'switched', label: selected.textContent?.trim?.() || label };
|
|
208
348
|
}
|
|
209
|
-
|
|
210
|
-
const label = opt.textContent?.trim?.() || null;
|
|
211
|
-
dispatchClickSequence(opt);
|
|
212
|
-
await sleep(STEP_WAIT_MS);
|
|
213
|
-
closeOpenMenus();
|
|
214
|
-
return { status: already ? 'already-selected' : 'switched', label };
|
|
349
|
+
await sleep(100);
|
|
215
350
|
}
|
|
351
|
+
const result = failure('selection-unverified');
|
|
216
352
|
closeOpenMenus();
|
|
217
|
-
return
|
|
218
|
-
}
|
|
353
|
+
return result;
|
|
354
|
+
};
|
|
219
355
|
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
356
|
+
// Current ChatGPT exposes a standalone Pro or Thinking composer pill whose
|
|
357
|
+
// controlled menu contains the effort levels. Prefer this ownership boundary
|
|
358
|
+
// before probing older model-picker layouts.
|
|
359
|
+
const COMPOSER_EFFORT_PILL_SELECTORS = [
|
|
360
|
+
'form button.__composer-pill[aria-haspopup="menu"]',
|
|
361
|
+
'[data-testid="composer-footer-actions"] button[aria-haspopup="menu"]',
|
|
362
|
+
'.__composer-pill-composite button[aria-haspopup="menu"]',
|
|
363
|
+
];
|
|
364
|
+
const findComposerEffortPill = () => {
|
|
365
|
+
const candidates = [];
|
|
366
|
+
const seen = new Set();
|
|
367
|
+
for (const selector of COMPOSER_EFFORT_PILL_SELECTORS) {
|
|
368
|
+
for (const button of document.querySelectorAll(selector)) {
|
|
369
|
+
if (seen.has(button) || !isVisible(button)) continue;
|
|
370
|
+
seen.add(button);
|
|
371
|
+
if (button.getAttribute?.('aria-haspopup') !== 'menu') continue;
|
|
372
|
+
if (button.getAttribute?.('data-testid') === 'model-switcher-dropdown-button') continue;
|
|
373
|
+
const label = normalize(
|
|
374
|
+
(button.getAttribute?.('aria-label') ?? '') + ' ' +
|
|
375
|
+
(button.getAttribute?.('data-testid') ?? '') + ' ' +
|
|
376
|
+
(button.textContent ?? ''),
|
|
377
|
+
);
|
|
378
|
+
if (
|
|
379
|
+
(TARGET_MODEL_KIND === 'pro' && hasToken(label, 'pro') && !hasToken(label, 'thinking')) ||
|
|
380
|
+
(TARGET_MODEL_KIND === 'thinking' && hasToken(label, 'thinking') && !hasToken(label, 'pro')) ||
|
|
381
|
+
(!TARGET_MODEL_KIND && hasToken(label, 'thinking'))
|
|
382
|
+
) {
|
|
383
|
+
return button;
|
|
384
|
+
}
|
|
385
|
+
candidates.push(button);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const composerPills = candidates.filter((button) =>
|
|
389
|
+
button.matches?.('button.__composer-pill'),
|
|
390
|
+
);
|
|
391
|
+
return composerPills.length === 1 ? composerPills[0] : null;
|
|
392
|
+
};
|
|
226
393
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
394
|
+
const composerEffortPill = findComposerEffortPill();
|
|
395
|
+
if (composerEffortPill) {
|
|
396
|
+
if (composerEffortPill.getAttribute?.('aria-expanded') !== 'true') {
|
|
397
|
+
dispatchClickSequence(composerEffortPill);
|
|
398
|
+
await sleep(INITIAL_WAIT_MS);
|
|
399
|
+
}
|
|
400
|
+
const deadline = performance.now() + MAX_WAIT_MS;
|
|
401
|
+
while (performance.now() < deadline) {
|
|
402
|
+
const menu = findVisibleEffortMenu(composerEffortPill);
|
|
403
|
+
if (menu) {
|
|
404
|
+
return selectAndVerify(composerEffortPill, () => {
|
|
405
|
+
const currentMenu = findVisibleEffortMenu(composerEffortPill);
|
|
406
|
+
return currentMenu ? findOptionInMenu(currentMenu) : null;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
await sleep(100);
|
|
410
|
+
}
|
|
411
|
+
const result = failure('menu-not-found');
|
|
412
|
+
closeOpenMenus();
|
|
413
|
+
return result;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Older ChatGPT layouts attach effort controls to rows inside the model
|
|
417
|
+
// picker. Keep these compatibility paths after the standalone pill owner.
|
|
230
418
|
const findEffortRow = (node) => {
|
|
231
419
|
let current = node instanceof HTMLElement ? node.parentElement : null;
|
|
232
420
|
while (current && current !== document.body) {
|
|
@@ -291,12 +479,8 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
291
479
|
}
|
|
292
480
|
return false;
|
|
293
481
|
};
|
|
294
|
-
const hasStableBox = (node) => {
|
|
295
|
-
const r = node.getBoundingClientRect?.();
|
|
296
|
-
return Boolean(r && r.width > 0 && r.height > 0 && node.getAttribute?.('aria-hidden') !== 'true');
|
|
297
|
-
};
|
|
298
482
|
const pickSingleStableTrailing = (trailings) => {
|
|
299
|
-
const visible = trailings.filter((
|
|
483
|
+
const visible = trailings.filter((trailing) => isVisible(trailing));
|
|
300
484
|
return visible.length === 1 ? visible[0] : null;
|
|
301
485
|
};
|
|
302
486
|
const pickTrailingForCurrentModel = () => {
|
|
@@ -317,7 +501,7 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
317
501
|
|
|
318
502
|
const modelBtn = findModelButton();
|
|
319
503
|
if (!modelBtn) {
|
|
320
|
-
return
|
|
504
|
+
return failure('chip-not-found');
|
|
321
505
|
}
|
|
322
506
|
// Open model menu (idempotent — leaves it open if already open).
|
|
323
507
|
if (modelBtn.getAttribute('aria-expanded') !== 'true') {
|
|
@@ -325,6 +509,49 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
325
509
|
await sleep(INITIAL_WAIT_MS);
|
|
326
510
|
}
|
|
327
511
|
|
|
512
|
+
// ---------- COMPATIBILITY UI: unified "Intelligence" effort picker ----------
|
|
513
|
+
// One observed ChatGPT layout replaced the per-model trailing buttons with a single
|
|
514
|
+
// "Intelligence" menu ([data-testid="composer-intelligence-picker-content"]),
|
|
515
|
+
// whose role="menuitemradio" rows are the effort tiers. For the Pro model the
|
|
516
|
+
// combined "Pro Extended" row carries aria-checked when active, and Pro
|
|
517
|
+
// sub-options live behind
|
|
518
|
+
// [data-testid="composer-intelligence-pro-thinking-effort-trigger"]. We
|
|
519
|
+
// confirm Pro Extended by the radio's checked state (real proof), never by
|
|
520
|
+
// the composer-pill label. The new non-pro tiers (Instant/Medium/High/Extra
|
|
521
|
+
// High) don't map cleanly onto light/standard/extended/heavy, so we only
|
|
522
|
+
// drive this picker for the strict Pro Extended target and let other levels
|
|
523
|
+
// fall through to the legacy paths below.
|
|
524
|
+
if (TARGET_MODEL_KIND === 'pro' && TARGET_LEVEL === 'extended') {
|
|
525
|
+
const matchesProExtended = (node) => {
|
|
526
|
+
const text = normalize(
|
|
527
|
+
(node?.textContent ?? '') + ' ' + (node?.getAttribute?.('aria-label') ?? ''),
|
|
528
|
+
);
|
|
529
|
+
return text.includes('pro') && text.includes('extended');
|
|
530
|
+
};
|
|
531
|
+
const findProExtendedOption = () => {
|
|
532
|
+
const menu = document.querySelector(INTELLIGENCE_MENU_SELECTOR);
|
|
533
|
+
if (!isVisible(menu)) return null;
|
|
534
|
+
for (const item of menu.querySelectorAll(
|
|
535
|
+
'[role="menuitemradio"], [role="menuitem"], [role="option"]',
|
|
536
|
+
)) {
|
|
537
|
+
if (matchesProExtended(item)) return item;
|
|
538
|
+
}
|
|
539
|
+
return null;
|
|
540
|
+
};
|
|
541
|
+
let proExtended = null;
|
|
542
|
+
const intelligenceDeadline = performance.now() + INTELLIGENCE_WAIT_MS;
|
|
543
|
+
while (performance.now() < intelligenceDeadline) {
|
|
544
|
+
proExtended = findProExtendedOption();
|
|
545
|
+
if (proExtended) break;
|
|
546
|
+
await sleep(100);
|
|
547
|
+
}
|
|
548
|
+
if (proExtended) {
|
|
549
|
+
return selectAndVerify(modelBtn, findProExtendedOption);
|
|
550
|
+
}
|
|
551
|
+
// Intelligence menu absent (older UI) or its Pro Extended row is missing:
|
|
552
|
+
// fall through to the legacy trailing-button path below.
|
|
553
|
+
}
|
|
554
|
+
|
|
328
555
|
let trailing = null;
|
|
329
556
|
const trailingDeadline = performance.now() + MAX_WAIT_MS;
|
|
330
557
|
while (performance.now() < trailingDeadline) {
|
|
@@ -333,12 +560,14 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
333
560
|
await sleep(100);
|
|
334
561
|
}
|
|
335
562
|
if (!trailing) {
|
|
563
|
+
const result = failure('chip-not-found');
|
|
336
564
|
closeOpenMenus();
|
|
337
|
-
return
|
|
565
|
+
return result;
|
|
338
566
|
}
|
|
339
567
|
if (trailing.kindNotFound) {
|
|
568
|
+
const result = failure('model-kind-not-found', { modelKind: TARGET_MODEL_KIND });
|
|
340
569
|
closeOpenMenus();
|
|
341
|
-
return
|
|
570
|
+
return result;
|
|
342
571
|
}
|
|
343
572
|
|
|
344
573
|
dispatchClickSequence(trailing);
|
|
@@ -349,18 +578,15 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
349
578
|
const resolveEffortMenu = () => {
|
|
350
579
|
const id = trailing.getAttribute('aria-controls');
|
|
351
580
|
if (id) {
|
|
352
|
-
const node = document.getElementById(id);
|
|
353
|
-
if (node) return node;
|
|
581
|
+
const node = document.getElementById?.(id);
|
|
582
|
+
if (isEffortMenu(node)) return node;
|
|
354
583
|
}
|
|
355
|
-
const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR
|
|
584
|
+
const menus = document.querySelectorAll(MENU_CONTAINER_SELECTOR);
|
|
356
585
|
let best = null;
|
|
357
586
|
for (const menu of menus) {
|
|
358
587
|
if (menu === modelBtn || menu.contains(trailing)) continue;
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
for (const tokens of Object.values(LEVEL_TOKENS)) {
|
|
362
|
-
if (tokens.some((tok) => text.includes(String(tok).toLowerCase()))) hits += 1;
|
|
363
|
-
}
|
|
588
|
+
if (!isVisible(menu)) continue;
|
|
589
|
+
const hits = countEffortLevels(menu);
|
|
364
590
|
if (hits >= 2 && (!best || hits > best.hits)) best = { menu, hits };
|
|
365
591
|
}
|
|
366
592
|
return best?.menu ?? null;
|
|
@@ -374,22 +600,15 @@ function buildThinkingTimeExpression(level, desiredModel) {
|
|
|
374
600
|
await sleep(100);
|
|
375
601
|
}
|
|
376
602
|
if (!effortMenu) {
|
|
603
|
+
const result = failure('menu-not-found');
|
|
377
604
|
closeOpenMenus();
|
|
378
|
-
return
|
|
605
|
+
return result;
|
|
379
606
|
}
|
|
380
607
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
const already = optionIsSelected(targetOption);
|
|
388
|
-
const label = targetOption.textContent?.trim?.() || null;
|
|
389
|
-
dispatchClickSequence(targetOption);
|
|
390
|
-
await sleep(STEP_WAIT_MS);
|
|
391
|
-
closeOpenMenus();
|
|
392
|
-
return { status: already ? 'already-selected' : 'switched', label };
|
|
608
|
+
return selectAndVerify(trailing, () => {
|
|
609
|
+
const currentMenu = resolveEffortMenu();
|
|
610
|
+
return currentMenu ? findOptionInMenu(currentMenu) : null;
|
|
611
|
+
});
|
|
393
612
|
})()`;
|
|
394
613
|
}
|
|
395
614
|
export function buildThinkingTimeExpressionForTest(level = "extended", desiredModel) {
|
|
@@ -66,6 +66,25 @@ export async function writeTextBrowserArtifact(params) {
|
|
|
66
66
|
sourceUrl: params.sourceUrl,
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
|
+
export async function writeBinaryBrowserArtifact(params) {
|
|
70
|
+
if (!params.sessionId || params.contents.length === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const dir = resolveSessionArtifactsDir(params.sessionId);
|
|
74
|
+
await fs.mkdir(dir, { recursive: true });
|
|
75
|
+
const filename = sanitizePathSegment(params.filename, "artifact.bin");
|
|
76
|
+
const targetPath = await resolveUniquePath(path.join(dir, filename));
|
|
77
|
+
await fs.writeFile(targetPath, params.contents);
|
|
78
|
+
params.logger?.(`[browser] Saved ${params.kind} artifact to ${targetPath}`);
|
|
79
|
+
return {
|
|
80
|
+
kind: params.kind,
|
|
81
|
+
path: targetPath,
|
|
82
|
+
label: params.label,
|
|
83
|
+
mimeType: params.mimeType,
|
|
84
|
+
sizeBytes: params.contents.length,
|
|
85
|
+
sourceUrl: params.sourceUrl,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
69
88
|
function isToolOnlyPlaceholder(text) {
|
|
70
89
|
const normalized = text.toLowerCase().replace(/\s+/g, " ").trim();
|
|
71
90
|
return (normalized === "called tool" ||
|