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.
- package/README.md +30 -6
- package/agents/gpt-pro.md +2 -2
- package/native/browser-session-store.cjs +30 -2
- package/native/chatgpt-client-selection.cjs +79 -35
- package/native/chatgpt-client-ui.cjs +626 -197
- package/native/chatgpt-client.cjs +65 -6
- package/native/cli.cjs +121 -4
- package/native/file-transfer.cjs +10 -1
- package/native/host-helpers.cjs +1 -1
- package/native/host.cjs +424 -7
- package/native/mcp-server.cjs +1 -1
- package/native/oracle-cli.cjs +68 -7
- package/native/oracle-host.cjs +46 -2
- package/native/oracle-jobs.cjs +5 -3
- package/native/socket-permissions.cjs +114 -0
- package/native/tool-scope.cjs +6 -2
- package/native/video-recorder.cjs +444 -0
- package/native/workflow-definition.cjs +5 -0
- package/package.json +3 -3
- package/pi-extension/surf.ts +31 -2
- package/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +11 -7
|
@@ -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
|
|
174
|
-
const
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
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
|
|
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
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
testId: item.getAttribute?.('data-testid') || null,
|
|
316
|
-
selected,
|
|
584
|
+
...item,
|
|
585
|
+
selected: node.getAttribute?.('aria-checked') === 'true' || state === 'checked',
|
|
317
586
|
};
|
|
318
|
-
})
|
|
319
|
-
|
|
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
|
|
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
|
|
329
|
-
if (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 {
|
|
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
|
|
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
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
|
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
|
|
693
|
+
return matches.length === 1 ? dispatchClickSequence(matches[0]) : false;
|
|
358
694
|
})()`,
|
|
695
|
+
signal,
|
|
359
696
|
);
|
|
360
697
|
}
|
|
361
698
|
|
|
362
|
-
async function
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
)
|
|
372
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
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
|
|
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
|
|
403
|
-
if (picker?.
|
|
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
|
-
|
|
412
|
-
const
|
|
413
|
-
if (
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
)
|
|
422
|
-
|
|
423
|
-
|
|
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,
|