surf-cli 2.10.0 → 2.11.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 +13 -0
- package/native/chatgpt-client-response.cjs +336 -0
- package/native/chatgpt-client-selection.cjs +119 -0
- package/native/chatgpt-client-ui.cjs +481 -0
- package/native/chatgpt-client.cjs +252 -664
- package/native/cli.cjs +53 -0
- package/native/host-helpers.cjs +26 -1
- package/native/host-sessions.cjs +1 -0
- package/native/host.cjs +29 -5
- package/native/oracle-cli.cjs +434 -0
- package/native/oracle-context.cjs +311 -0
- package/native/oracle-host.cjs +301 -0
- package/native/oracle-jobs.cjs +253 -0
- package/native/workflow-definition.cjs +1 -0
- package/package.json +1 -1
- package/skills/surf/SKILL.md +31 -0
package/README.md
CHANGED
|
@@ -492,6 +492,19 @@ surf aistudio.build "crm dashboard" --output ./out # Extract zip to d
|
|
|
492
492
|
surf aistudio.build "game" --keep-open --timeout 600 # Keep tab open, 10min timeout
|
|
493
493
|
```
|
|
494
494
|
|
|
495
|
+
#### Oracle
|
|
496
|
+
|
|
497
|
+
Use `surf oracle` for a durable, local ChatGPT consult instead of a quick `surf chatgpt` one-shot. It persists jobs by conversation URL, supports repeatable file-context globs, and verifies requested model and reasoning effort before submission.
|
|
498
|
+
|
|
499
|
+
```bash
|
|
500
|
+
surf oracle ask "review this change" --files "src/**/*.ts" --model pro --effort extended --detach --json
|
|
501
|
+
surf oracle status <job-id> --json
|
|
502
|
+
surf oracle result <job-id> --wait --json
|
|
503
|
+
surf oracle follow <job-id> "challenge that recommendation" --detach --json
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
Only one oracle job can be in flight. Sensitive filename patterns and gitignored context are blocked unless `--allow-sensitive` is explicit.
|
|
507
|
+
|
|
495
508
|
Each AI tool uses your existing browser login - no API keys needed. Just be logged into the respective service in Chrome (chatgpt.com, gemini.google.com, perplexity.ai, x.com, or aistudio.google.com).
|
|
496
509
|
|
|
497
510
|
**Grok troubleshooting:** If queries fail, run `surf grok --validate` to check if the UI structure changed. Use `--save-models` to update the model cache in `surf.json`. Default model is `fast`.
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
const { throwIfAborted } = require("./abort.cjs");
|
|
2
|
+
const { SELECTORS, delay, evaluate } = require("./chatgpt-client-ui.cjs");
|
|
3
|
+
|
|
4
|
+
function normalizePromptEcho(prompt) {
|
|
5
|
+
return String(prompt || "")
|
|
6
|
+
.replace(/\s+/g, " ")
|
|
7
|
+
.trim()
|
|
8
|
+
.slice(0, 200);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function matchesPromptEcho(text, promptEcho) {
|
|
12
|
+
const expected = normalizePromptEcho(promptEcho);
|
|
13
|
+
return expected.length > 0 && normalizePromptEcho(text) === expected;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function cleanChatGPTResponseText(rawText) {
|
|
17
|
+
if (!rawText) return "";
|
|
18
|
+
|
|
19
|
+
const chromeLines = new Set([
|
|
20
|
+
"copy",
|
|
21
|
+
"good response",
|
|
22
|
+
"bad response",
|
|
23
|
+
"read aloud",
|
|
24
|
+
"edit",
|
|
25
|
+
"retry",
|
|
26
|
+
"continue generating",
|
|
27
|
+
"share",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const lines = [];
|
|
31
|
+
let inCodeFence = false;
|
|
32
|
+
|
|
33
|
+
for (const line of String(rawText).replace(/\r\n?/g, "\n").split("\n")) {
|
|
34
|
+
const trimmed = line.trim();
|
|
35
|
+
const isFenceLine = trimmed.startsWith("```");
|
|
36
|
+
const normalizedLine = inCodeFence || isFenceLine ? line.replace(/[\t ]+$/g, "") : line;
|
|
37
|
+
|
|
38
|
+
lines.push({
|
|
39
|
+
text: normalizedLine,
|
|
40
|
+
trimmed,
|
|
41
|
+
isChrome: trimmed.length > 0 && chromeLines.has(trimmed.toLowerCase()),
|
|
42
|
+
inCodeFence,
|
|
43
|
+
isFenceLine,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (isFenceLine) {
|
|
47
|
+
inCodeFence = !inCodeFence;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
52
|
+
lines.shift();
|
|
53
|
+
}
|
|
54
|
+
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
55
|
+
lines.pop();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let trailingChromeStart = lines.length;
|
|
59
|
+
while (trailingChromeStart > 0) {
|
|
60
|
+
const line = lines[trailingChromeStart - 1];
|
|
61
|
+
if (line.inCodeFence || line.isFenceLine || !line.isChrome) break;
|
|
62
|
+
trailingChromeStart--;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const trailingChromeCount = lines.length - trailingChromeStart;
|
|
66
|
+
if (trailingChromeCount >= 2) {
|
|
67
|
+
lines.splice(trailingChromeStart);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
while (lines.length > 0 && lines[0].trimmed.length === 0) {
|
|
71
|
+
lines.shift();
|
|
72
|
+
}
|
|
73
|
+
while (lines.length > 0 && lines[lines.length - 1].trimmed.length === 0) {
|
|
74
|
+
lines.pop();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return lines.map((line) => line.text).join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function extractLatestAssistantSnapshot(candidates) {
|
|
81
|
+
if (!Array.isArray(candidates)) return null;
|
|
82
|
+
|
|
83
|
+
let latestEmptyAssistant = null;
|
|
84
|
+
|
|
85
|
+
for (let i = candidates.length - 1; i >= 0; i--) {
|
|
86
|
+
const candidate = candidates[i];
|
|
87
|
+
if (!candidate?.isAssistant) continue;
|
|
88
|
+
|
|
89
|
+
const snapshot = {
|
|
90
|
+
...candidate,
|
|
91
|
+
text: cleanChatGPTResponseText(candidate?.text || ""),
|
|
92
|
+
turnIndex: i,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (snapshot.text) {
|
|
96
|
+
return snapshot;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (!latestEmptyAssistant) {
|
|
100
|
+
latestEmptyAssistant = snapshot;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return latestEmptyAssistant;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeResponseSnapshot(rawSnapshot) {
|
|
108
|
+
const candidates = rawSnapshot?.candidates;
|
|
109
|
+
return {
|
|
110
|
+
latestAssistant: extractLatestAssistantSnapshot(candidates),
|
|
111
|
+
assistantCount: Array.isArray(candidates)
|
|
112
|
+
? candidates.filter((candidate) => candidate?.isAssistant).length
|
|
113
|
+
: 0,
|
|
114
|
+
stopVisible: Boolean(rawSnapshot?.stopVisible),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isNewAssistantContent(
|
|
119
|
+
latestAssistant,
|
|
120
|
+
baselineAssistant,
|
|
121
|
+
assistantCount = 0,
|
|
122
|
+
baselineAssistantCount = 0,
|
|
123
|
+
) {
|
|
124
|
+
if (!latestAssistant) return false;
|
|
125
|
+
if (!baselineAssistant) return true;
|
|
126
|
+
if (
|
|
127
|
+
latestAssistant.messageId &&
|
|
128
|
+
baselineAssistant.messageId &&
|
|
129
|
+
latestAssistant.messageId !== baselineAssistant.messageId
|
|
130
|
+
) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const currentText = latestAssistant.text || "";
|
|
135
|
+
const baselineText = baselineAssistant.text || "";
|
|
136
|
+
|
|
137
|
+
if (assistantCount > baselineAssistantCount) {
|
|
138
|
+
if (latestAssistant.turnIndex !== baselineAssistant.turnIndex) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
if (currentText !== baselineText) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return currentText !== baselineText;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isChatGPTResponseComplete(snapshot, stableCycles, stableMs) {
|
|
151
|
+
if (!snapshot?.text) return false;
|
|
152
|
+
if (snapshot.stopVisible) return false;
|
|
153
|
+
if (snapshot.hasFinishedActions) return true;
|
|
154
|
+
return stableCycles >= 6 && stableMs >= 1200;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scopeSnapshotToPrompt(rawSnapshot, promptEcho) {
|
|
158
|
+
const candidates = Array.isArray(rawSnapshot?.candidates) ? rawSnapshot.candidates : [];
|
|
159
|
+
let userIndex = -1;
|
|
160
|
+
for (let index = candidates.length - 1; index >= 0; index--) {
|
|
161
|
+
const candidate = candidates[index];
|
|
162
|
+
if (candidate?.isUser && matchesPromptEcho(candidate.text, promptEcho)) {
|
|
163
|
+
userIndex = index;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (userIndex === -1) return { ...rawSnapshot, candidates: [] };
|
|
168
|
+
|
|
169
|
+
const following = candidates.slice(userIndex + 1);
|
|
170
|
+
const nextUserIndex = following.findIndex((candidate) => candidate?.isUser);
|
|
171
|
+
return {
|
|
172
|
+
...rawSnapshot,
|
|
173
|
+
candidates: nextUserIndex === -1 ? following : following.slice(0, nextUserIndex),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function readChatGPTResponseSnapshot(cdp) {
|
|
178
|
+
return evaluate(
|
|
179
|
+
cdp,
|
|
180
|
+
`(() => {
|
|
181
|
+
const scope = document.querySelector('main') || document;
|
|
182
|
+
const CONVERSATION_SELECTOR = ${JSON.stringify(SELECTORS.conversationTurn)};
|
|
183
|
+
const ASSISTANT_SELECTOR = ${JSON.stringify(SELECTORS.assistantMessage)};
|
|
184
|
+
const CONTENT_SELECTORS = ${JSON.stringify(SELECTORS.assistantContent.split(", "))};
|
|
185
|
+
const STOP_SELECTOR = ${JSON.stringify(SELECTORS.stopButton)};
|
|
186
|
+
const FINISHED_SELECTOR = ${JSON.stringify(SELECTORS.finishedActions)};
|
|
187
|
+
|
|
188
|
+
const toCandidate = (turnNode, messageRoot = null) => {
|
|
189
|
+
const resolvedMessageRoot = messageRoot || (turnNode.matches?.(ASSISTANT_SELECTOR)
|
|
190
|
+
? turnNode
|
|
191
|
+
: turnNode.querySelector(ASSISTANT_SELECTOR));
|
|
192
|
+
const authorRoot = turnNode.matches?.('[data-message-author-role], [data-turn]')
|
|
193
|
+
? turnNode
|
|
194
|
+
: turnNode.querySelector('[data-message-author-role], [data-turn]');
|
|
195
|
+
const searchRoot = resolvedMessageRoot || authorRoot || turnNode;
|
|
196
|
+
let contentRoot = null;
|
|
197
|
+
|
|
198
|
+
for (const selector of CONTENT_SELECTORS) {
|
|
199
|
+
const match = selector === '[dir="auto"]'
|
|
200
|
+
? (searchRoot.matches?.(selector) ? searchRoot : null)
|
|
201
|
+
: (searchRoot.matches?.(selector) ? searchRoot : searchRoot.querySelector(selector));
|
|
202
|
+
if (match) {
|
|
203
|
+
contentRoot = match;
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const role =
|
|
209
|
+
resolvedMessageRoot?.getAttribute('data-message-author-role') ||
|
|
210
|
+
authorRoot?.getAttribute('data-message-author-role') ||
|
|
211
|
+
turnNode.getAttribute('data-message-author-role') ||
|
|
212
|
+
null;
|
|
213
|
+
const turn =
|
|
214
|
+
resolvedMessageRoot?.getAttribute('data-turn') ||
|
|
215
|
+
authorRoot?.getAttribute('data-turn') ||
|
|
216
|
+
turnNode.getAttribute('data-turn') ||
|
|
217
|
+
null;
|
|
218
|
+
const isAssistant =
|
|
219
|
+
role === 'assistant' ||
|
|
220
|
+
turn === 'assistant' ||
|
|
221
|
+
resolvedMessageRoot !== null;
|
|
222
|
+
const isUser = role === 'user' || turn === 'user';
|
|
223
|
+
const text = (contentRoot || turnNode).innerText || (contentRoot || turnNode).textContent || '';
|
|
224
|
+
const messageId =
|
|
225
|
+
resolvedMessageRoot?.getAttribute('data-message-id') ||
|
|
226
|
+
turnNode.getAttribute('data-message-id') ||
|
|
227
|
+
null;
|
|
228
|
+
const hasFinishedActions = Boolean(turnNode.querySelector(FINISHED_SELECTOR));
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
role,
|
|
232
|
+
turn,
|
|
233
|
+
isAssistant,
|
|
234
|
+
isUser,
|
|
235
|
+
text,
|
|
236
|
+
messageId,
|
|
237
|
+
hasFinishedActions,
|
|
238
|
+
};
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
let candidates = Array.from(scope.querySelectorAll(CONVERSATION_SELECTOR)).map((turnNode) =>
|
|
242
|
+
toCandidate(turnNode)
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (candidates.length === 0) {
|
|
246
|
+
candidates = Array.from(scope.querySelectorAll(ASSISTANT_SELECTOR)).map((messageRoot) =>
|
|
247
|
+
toCandidate(messageRoot, messageRoot)
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
candidates,
|
|
253
|
+
stopVisible: Boolean(scope.querySelector(STOP_SELECTOR)),
|
|
254
|
+
};
|
|
255
|
+
})()`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function waitForResponse(
|
|
260
|
+
cdp,
|
|
261
|
+
timeoutMs = 2700000,
|
|
262
|
+
baselineAssistant,
|
|
263
|
+
baselineAssistantCount,
|
|
264
|
+
signal,
|
|
265
|
+
promptEcho,
|
|
266
|
+
) {
|
|
267
|
+
throwIfAborted(signal);
|
|
268
|
+
const deadline = Date.now() + timeoutMs;
|
|
269
|
+
let previousText = baselineAssistant?.text || "";
|
|
270
|
+
let stableCycles = 0;
|
|
271
|
+
let lastChangeAt = Date.now();
|
|
272
|
+
|
|
273
|
+
while (Date.now() < deadline) {
|
|
274
|
+
const rawSnapshot = await readChatGPTResponseSnapshot(cdp);
|
|
275
|
+
const snapshot = promptEcho ? scopeSnapshotToPrompt(rawSnapshot, promptEcho) : rawSnapshot;
|
|
276
|
+
|
|
277
|
+
if (!snapshot) {
|
|
278
|
+
await delay(400, signal);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { latestAssistant, assistantCount, stopVisible } = normalizeResponseSnapshot(snapshot);
|
|
283
|
+
const currentText = latestAssistant?.text || "";
|
|
284
|
+
const hasNewAssistantContent = isNewAssistantContent(
|
|
285
|
+
latestAssistant,
|
|
286
|
+
baselineAssistant,
|
|
287
|
+
assistantCount,
|
|
288
|
+
baselineAssistantCount,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (!hasNewAssistantContent) {
|
|
292
|
+
await delay(400, signal);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (currentText !== previousText) {
|
|
297
|
+
previousText = currentText;
|
|
298
|
+
stableCycles = 0;
|
|
299
|
+
lastChangeAt = Date.now();
|
|
300
|
+
} else if (currentText) {
|
|
301
|
+
stableCycles++;
|
|
302
|
+
} else {
|
|
303
|
+
stableCycles = 0;
|
|
304
|
+
lastChangeAt = Date.now();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const stableMs = Date.now() - lastChangeAt;
|
|
308
|
+
const completionSnapshot = latestAssistant
|
|
309
|
+
? { ...latestAssistant, stopVisible }
|
|
310
|
+
: { text: "", stopVisible, hasFinishedActions: false };
|
|
311
|
+
|
|
312
|
+
if (isChatGPTResponseComplete(completionSnapshot, stableCycles, stableMs)) {
|
|
313
|
+
return {
|
|
314
|
+
text: latestAssistant.text,
|
|
315
|
+
messageId: latestAssistant.messageId,
|
|
316
|
+
turnIndex: latestAssistant.turnIndex,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
await delay(400, signal);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw new Error("Response timeout");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
module.exports = {
|
|
327
|
+
cleanChatGPTResponseText,
|
|
328
|
+
extractLatestAssistantSnapshot,
|
|
329
|
+
isChatGPTResponseComplete,
|
|
330
|
+
isNewAssistantContent,
|
|
331
|
+
matchesPromptEcho,
|
|
332
|
+
normalizePromptEcho,
|
|
333
|
+
normalizeResponseSnapshot,
|
|
334
|
+
readChatGPTResponseSnapshot,
|
|
335
|
+
waitForResponse,
|
|
336
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const CHATGPT_EFFORT_CHOICES = ["light", "standard", "extended", "heavy"];
|
|
2
|
+
|
|
3
|
+
function normalizeChatGPTModelChoice(desiredModel) {
|
|
4
|
+
const normalized = String(desiredModel || "")
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[^a-z0-9]/g, "");
|
|
7
|
+
|
|
8
|
+
if (["instant", "gpt53"].includes(normalized)) return "instant";
|
|
9
|
+
if (["thinking", "gpt54thinking"].includes(normalized)) return "thinking";
|
|
10
|
+
if (["pro", "gpt54pro"].includes(normalized)) return "pro";
|
|
11
|
+
|
|
12
|
+
return normalized;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeChatGPTEffortChoice(desiredEffort) {
|
|
16
|
+
const normalized = String(desiredEffort || "").toLowerCase().trim();
|
|
17
|
+
return CHATGPT_EFFORT_CHOICES.includes(normalized) ? normalized : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function normalizedWords(value) {
|
|
21
|
+
return String(value || "")
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.replace(/[^a-z0-9]+/g, " ")
|
|
24
|
+
.trim()
|
|
25
|
+
.split(/\s+/)
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function modelCandidateMatches(item, targetModel) {
|
|
30
|
+
const values = [item?.label, item?.testId?.replace(/^model-switcher-/, "")].filter(Boolean);
|
|
31
|
+
return values.some((value) => {
|
|
32
|
+
if (normalizeChatGPTModelChoice(value) === targetModel) return true;
|
|
33
|
+
const variants = ["instant", "thinking", "pro"].filter((variant) =>
|
|
34
|
+
normalizedWords(value).includes(variant),
|
|
35
|
+
);
|
|
36
|
+
return variants.length === 1 && variants[0] === targetModel;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function effortCandidateMatches(item, targetEffort) {
|
|
41
|
+
const variants = new Set(
|
|
42
|
+
[item?.label, item?.testId]
|
|
43
|
+
.flatMap((value) => normalizedWords(value))
|
|
44
|
+
.filter((word) => CHATGPT_EFFORT_CHOICES.includes(word)),
|
|
45
|
+
);
|
|
46
|
+
return variants.size === 1 && variants.has(targetEffort);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function uniqueMatch(items, matches) {
|
|
50
|
+
if (!Array.isArray(items)) return null;
|
|
51
|
+
const candidates = items.filter(matches);
|
|
52
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function resolveChatGPTModelMenuOption(items, desiredModel) {
|
|
56
|
+
const targetModel = normalizeChatGPTModelChoice(desiredModel);
|
|
57
|
+
if (!targetModel) return null;
|
|
58
|
+
return uniqueMatch(
|
|
59
|
+
items,
|
|
60
|
+
(item) =>
|
|
61
|
+
item?.role === "menuitemradio" &&
|
|
62
|
+
typeof item?.testId === "string" &&
|
|
63
|
+
item.testId.startsWith("model-switcher-") &&
|
|
64
|
+
modelCandidateMatches(item, targetModel),
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function verifyChatGPTModelSelection(items, desiredModel) {
|
|
69
|
+
const targetModel = normalizeChatGPTModelChoice(desiredModel);
|
|
70
|
+
if (!targetModel) return null;
|
|
71
|
+
return uniqueMatch(
|
|
72
|
+
items,
|
|
73
|
+
(item) =>
|
|
74
|
+
(typeof item?.label === "string" || typeof item?.testId === "string") &&
|
|
75
|
+
modelCandidateMatches(item, targetModel),
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function resolveChatGPTEffortMenuOption(items, desiredEffort) {
|
|
80
|
+
const targetEffort = normalizeChatGPTEffortChoice(desiredEffort);
|
|
81
|
+
if (!targetEffort) return null;
|
|
82
|
+
return uniqueMatch(
|
|
83
|
+
items,
|
|
84
|
+
(item) =>
|
|
85
|
+
["button", "menuitem", "menuitemradio"].includes(item?.role) &&
|
|
86
|
+
effortCandidateMatches(item, targetEffort),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function verifyChatGPTEffortSelection(items, desiredEffort) {
|
|
91
|
+
const targetEffort = normalizeChatGPTEffortChoice(desiredEffort);
|
|
92
|
+
if (!targetEffort) return null;
|
|
93
|
+
return uniqueMatch(
|
|
94
|
+
items,
|
|
95
|
+
(item) =>
|
|
96
|
+
(typeof item?.label === "string" || typeof item?.testId === "string") &&
|
|
97
|
+
effortCandidateMatches(item, targetEffort),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function boundedOptionLabels(items) {
|
|
102
|
+
if (!Array.isArray(items)) return [];
|
|
103
|
+
return items
|
|
104
|
+
.map((item) => String(item?.label || "").replace(/\s+/g, " ").trim().slice(0, 80))
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.filter((label, index, labels) => labels.indexOf(label) === index)
|
|
107
|
+
.slice(0, 10);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
CHATGPT_EFFORT_CHOICES,
|
|
112
|
+
boundedOptionLabels,
|
|
113
|
+
normalizeChatGPTEffortChoice,
|
|
114
|
+
normalizeChatGPTModelChoice,
|
|
115
|
+
resolveChatGPTEffortMenuOption,
|
|
116
|
+
resolveChatGPTModelMenuOption,
|
|
117
|
+
verifyChatGPTEffortSelection,
|
|
118
|
+
verifyChatGPTModelSelection,
|
|
119
|
+
};
|