surf-cli 2.17.0 → 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 +28 -4
- 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 +313 -198
- package/native/chatgpt-client.cjs +7 -2
- package/native/cli.cjs +121 -4
- package/native/file-transfer.cjs +5 -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 +2 -2
- 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/scripts/install-native-host.cjs +53 -6
- package/skills/surf/SKILL.md +7 -3
package/README.md
CHANGED
|
@@ -146,6 +146,8 @@ surf remote list
|
|
|
146
146
|
|
|
147
147
|
`surf install --listen` persists the explicit Tailnet address in the native-host wrapper. Re-run `surf install` without `--listen` to remove it. The address must be a Tailscale IPv4 or IPv6 address with a port; Surf does not bind every interface. Remote listeners currently require a POSIX browser host and are not supported by Windows native-host wrappers.
|
|
148
148
|
|
|
149
|
+
For advanced local-only sharing, `surf install <extension-id> --socket-mode 660 --socket-group <group>` persists an opt-in group-owned socket. The default remains mode `600`; mode `660` gives every account in that group full Surf authority, so use a dedicated narrow group. Re-run `surf install` without these flags to clear persisted socket settings. Remote Surf credentials remain the revocable per-client alternative.
|
|
150
|
+
|
|
149
151
|
Keep Tailscale policy restrictions as defense in depth. For example:
|
|
150
152
|
|
|
151
153
|
```json
|
|
@@ -188,7 +190,7 @@ surf --remote "$SURF_REMOTE" --remote-credential "$SURF_REMOTE_CREDENTIAL" \
|
|
|
188
190
|
|
|
189
191
|
Client-local inputs are staged privately on the host and removed after the request. Client-local outputs are downloaded with size/hash verification and atomic destination replacement. `surf js --file` and `perf-audit --output` are handled by the client itself. `network.export` defaults to a generated client-local `.json`, `.jsonl`, or `.har` path. Gemini edits default to client-local `edited.png`. Successful remote actions transfer their automatic screenshot to a generated client-local path; `--auto-capture` on failure remains a separate screenshot and console diagnostic.
|
|
190
192
|
|
|
191
|
-
The remote single-file boundary supports one `upload` file, one ChatGPT attachment, or one Gemini attachment/edit input, plus one screenshot, network export, or Gemini image output. Transfers are limited to 256 MiB per file, 512 MiB and 32 files per connection, with 256 KiB decoded chunks. Remote `record`, `aistudio.build`, smoke screenshot directories, directory transfer, and multi-file inputs are intentionally rejected. A `remote:` path bypasses transfer and gives the trusted client direct authority over that absolute host path.
|
|
193
|
+
The remote single-file boundary supports one `upload` file, one ChatGPT attachment, or one Gemini attachment/edit input, plus one screenshot, network export, or Gemini image output. Transfers are limited to 256 MiB per file, 512 MiB and 32 files per connection, with 256 KiB decoded chunks. Remote `record`, `video`, `aistudio.build`, smoke screenshot directories, directory transfer, and multi-file inputs are intentionally rejected. A `remote:` path bypasses transfer and gives the trusted client direct authority over that absolute host path.
|
|
192
194
|
|
|
193
195
|
### Development Setup
|
|
194
196
|
|
|
@@ -378,10 +380,14 @@ SURF_SESSION=research surf screenshot # environment selector
|
|
|
378
380
|
surf session.list --refresh # all bindings + queue state
|
|
379
381
|
surf session.info research --refresh # target and scheduler details
|
|
380
382
|
surf session.close research # closes Surf-created target
|
|
383
|
+
surf session.cleanup --idle-after 1h --dry-run # preview forgotten sessions
|
|
384
|
+
surf session.cleanup --idle-after 1h # remove idle bindings
|
|
381
385
|
surf session.rebind research --tab-id 789 # adopt an existing tab
|
|
382
386
|
surf session.reopen research # recreate from last URL
|
|
383
387
|
```
|
|
384
388
|
|
|
389
|
+
`session.cleanup` is an explicit, one-shot cleanup operation; `--idle-after` is required. It inspects current bindings first, removes gone or stale records, and removes live inactive bindings older than the threshold. Only Surf-created targets are closed. Adopted targets remain open while their session binding is removed. Use `--dry-run` to report the exact bindings and target actions without changing the store or browser.
|
|
390
|
+
|
|
385
391
|
Commands for the same session tab run FIFO. Commands for different session tabs can overlap. Browser-wide mutations—such as creating, moving, closing, or focusing tabs/windows and writing cookies—wait for active tab lanes to drain. Add `--no-wait` to return `tab_busy` or `browser_busy` immediately instead of queueing.
|
|
386
392
|
|
|
387
393
|
Recovery errors print an exact command that can be copied directly:
|
|
@@ -440,6 +446,19 @@ surf record --rect 0,200,1440,800 --output /tmp/region.gif
|
|
|
440
446
|
|
|
441
447
|
`record` defaults to 2000ms at 10fps and writes to `/tmp/surf-record-*.gif` when no output is provided. `--duration` is capped at 10000ms and `--fps` is capped at 30. `--trigger` supports `click:<selector>`, `scroll:up|down|left|right|top|bottom`, and `scroll:<selector>` to scroll a container to the bottom before capture. `--rect` crops the GIF using `x,y,width,height`. ImageMagick must be available as `magick` or `convert`.
|
|
442
448
|
|
|
449
|
+
### Local Video Recording
|
|
450
|
+
|
|
451
|
+
Capture a local tab as WebM/VP9 with ffmpeg:
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
surf video start ./demo.webm --fps 30
|
|
455
|
+
surf video status
|
|
456
|
+
surf video stop
|
|
457
|
+
surf video restart ./take-2.webm --fps 60
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
`--fps` defaults to 30 and is capped at 60. Video recording is local-only and requires `ffmpeg` on `PATH`.
|
|
461
|
+
|
|
443
462
|
### Animation Audit
|
|
444
463
|
|
|
445
464
|
Sample matching elements over time and return a bounded JSON timeline for agent inspection:
|
|
@@ -526,7 +545,9 @@ surf aistudio.build "game" --keep-open --timeout 600 # Keep tab open, 1
|
|
|
526
545
|
|
|
527
546
|
#### Oracle
|
|
528
547
|
|
|
529
|
-
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, one direct local attachment with `--file`, and verifies requested model and reasoning effort before submission. Add `--github` when the consult needs the ChatGPT Chat tab and connected GitHub tool. ChatGPT model aliases include `
|
|
548
|
+
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, one direct local attachment with `--file`, and verifies requested model and reasoning effort before submission. Add `--github` when the consult needs the ChatGPT Chat tab and connected GitHub tool. ChatGPT model aliases include `gpt-6-astra`, `latest`, `gpt-5.6-sol`, and `gpt-5.5`; `latest` is an explicit floating choice, while `gpt-6-astra` must read back as model 6 before submission. Accepted efforts are `instant`, `medium`, `high`, `xhigh`/`extra-high`, and `pro`. Use `--model gpt-6-astra --effort pro` for GPT-6 Astra with Pro effort.
|
|
549
|
+
|
|
550
|
+
ChatGPT can hide the model version at lower effort settings. Use `--model latest` if you want floating model selection there; an explicit `gpt-6-astra` request fails closed when the UI cannot verify model 6.
|
|
530
551
|
|
|
531
552
|
```bash
|
|
532
553
|
surf oracle ask "review this change" --files "src/**/*.ts" --file ./design.md --model gpt-5.5 --effort pro --github --detach --json
|
|
@@ -807,6 +828,8 @@ SURF_REMOTE # Remote Surf endpoint as host:port (overrides SURF_SO
|
|
|
807
828
|
SURF_REMOTE_CREDENTIAL # Client Ed25519 credential for the selected remote endpoint
|
|
808
829
|
SURF_REMOTE_STATE_DIR # Host identity/authorization directory (default: ~/.surf/remote)
|
|
809
830
|
SURF_LISTEN # Native-host Tailnet bind address as <tailscale-ip>:<port>
|
|
831
|
+
SURF_SOCKET_MODE # Advanced POSIX local socket mode: 600 (default) or 660
|
|
832
|
+
SURF_SOCKET_GROUP # Group name or numeric gid required with mode 660
|
|
810
833
|
SURF_NODE_PATH # Path to node binary (for native host wrapper)
|
|
811
834
|
SURF_HOST_PATH # Path to native/host.cjs (for native host wrapper)
|
|
812
835
|
SURF_EXTENSION_PATH # Path to extension dist/ directory
|
|
@@ -820,6 +843,7 @@ SURF_EXTENSION_PATH # Path to extension dist/ directory
|
|
|
820
843
|
- `SURF_REMOTE_CREDENTIAL`: Credential used for mutual remote authentication. `--remote-credential <path>` overrides it.
|
|
821
844
|
- `SURF_REMOTE_STATE_DIR`: Advanced host-side override for the mode-0700 identity and client registry directory.
|
|
822
845
|
- `SURF_LISTEN`: Native-host listener address on the browser machine. Use `surf install ... --listen <tailscale-ip>:<port>` to persist it in that host's wrapper.
|
|
846
|
+
- `SURF_SOCKET_MODE` / `SURF_SOCKET_GROUP`: Advanced POSIX native-host settings. Use `surf install ... --socket-mode 660 --socket-group <group>` to persist group access; mode `660` grants full Surf authority to every member of that group.
|
|
823
847
|
- `SURF_NODE_PATH` / `SURF_HOST_PATH`: Package manager installs (e.g., Nix) that store binaries in non-standard locations
|
|
824
848
|
- `SURF_EXTENSION_PATH`: Package managers that create stable symlinks instead of changing paths on reinstall
|
|
825
849
|
|
|
@@ -996,9 +1020,9 @@ pi -e /path/to/surf-cli/pi-extension/surf.ts
|
|
|
996
1020
|
|
|
997
1021
|
It registers `surf_read`, `surf_screenshot`, `surf_click`, `surf_type`, `surf_tool`, and the `surf_oracle_*` tools. Browser calls use Surf's native-host socket, not shell commands. If `pi-subagents/background-work` is installed, the extension also reports active oracle jobs started by that Pi session. Pi still loads the browser tools when pi-subagents is not installed.
|
|
998
1022
|
|
|
999
|
-
The extension also registers a `surf-oracle` external-job provider when a Pi runtime exposes that provider bridge. The provider implements pi-subagents' external-job contract: `start`, `status`, `result`, and `reattach` operations that return `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the durable conversation URL, the captured result text as `output`, and failure code and message when present. It reads `options.model`, `options.effort`, `options.file`, and `options.github` for starts and follow-ups, so a Pi profile can request `model: gpt-
|
|
1023
|
+
The extension also registers a `surf-oracle` external-job provider when a Pi runtime exposes that provider bridge. The provider implements pi-subagents' external-job contract: `start`, `status`, `result`, and `reattach` operations that return `providerJobId`, a contract state (`queued`, `running`, `completed`, `failed`), the durable conversation URL, the captured result text as `output`, and failure code and message when present. It reads `options.model`, `options.effort`, `options.file`, and `options.github` for starts and follow-ups, so a Pi profile can request `model: gpt-6-astra` plus `effort: pro` and reach ChatGPT GPT-6 Astra with Pro effort through Surf, while `github: true` requires Chat mode and the connected GitHub tool. Capacity stays fail-closed: Surf returns the blocking job id instead of silently queueing a second ChatGPT job.
|
|
1000
1024
|
|
|
1001
|
-
When Surf is installed as a Pi package, it also exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, `options.model: gpt-
|
|
1025
|
+
When Surf is installed as a Pi package, it also exposes an optional `gpt-pro` package agent for `pi-subagents`. That profile uses `runner.type: external-job`, provider `surf-oracle`, `options.model: gpt-6-astra`, and `options.effort: pro`. Surf remains useful without Pi or `pi-subagents`; the package agent only wires Surf's browser-backed model alias into Pi's agent picker.
|
|
1002
1026
|
|
|
1003
1027
|
Shell-based agents should select a unique session with `SURF_SESSION` and call `surf session.ensure` before their first browser command. The optional Pi extension still uses its existing socket-tool interface; callers that coordinate several Pi workers should pass explicit tab targets until session selection is exposed by that integration.
|
|
1004
1028
|
|
package/agents/gpt-pro.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: gpt-pro
|
|
3
|
-
description: Surf GPT Pro advisor through ChatGPT GPT-
|
|
3
|
+
description: Surf GPT Pro advisor through ChatGPT GPT-6 Astra Pro web mode
|
|
4
4
|
runner:
|
|
5
5
|
type: external-job
|
|
6
6
|
provider: surf-oracle
|
|
7
7
|
options:
|
|
8
|
-
model: gpt-
|
|
8
|
+
model: gpt-6-astra
|
|
9
9
|
effort: pro
|
|
10
10
|
async: true
|
|
11
11
|
systemPromptMode: replace
|
|
@@ -9,6 +9,24 @@ const { surfError } = require("./surf-error.cjs");
|
|
|
9
9
|
|
|
10
10
|
const STORE_VERSION = 1;
|
|
11
11
|
const SESSION_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
12
|
+
const SESSION_DURATION_PATTERN = /^(\d+(?:\.\d+)?)(s|m|h|d)?$/i;
|
|
13
|
+
|
|
14
|
+
function parseDurationMs(value) {
|
|
15
|
+
if (typeof value === "boolean" || value === undefined || value === null) {
|
|
16
|
+
throw new Error("--idle-after requires a positive duration such as 30s, 5m, 1h, or 1d");
|
|
17
|
+
}
|
|
18
|
+
const match = String(value).trim().match(SESSION_DURATION_PATTERN);
|
|
19
|
+
if (!match) {
|
|
20
|
+
throw new Error("--idle-after must be a duration such as 30s, 5m, 1h, or 1d (plain seconds are also accepted)");
|
|
21
|
+
}
|
|
22
|
+
const amount = Number(match[1]);
|
|
23
|
+
const multiplier = { s: 1000, m: 60000, h: 3600000, d: 86400000 }[(match[2] || "s").toLowerCase()];
|
|
24
|
+
const milliseconds = amount * multiplier;
|
|
25
|
+
if (!Number.isFinite(milliseconds) || milliseconds <= 0) {
|
|
26
|
+
throw new Error("--idle-after must be a positive duration");
|
|
27
|
+
}
|
|
28
|
+
return Math.max(1, Math.round(milliseconds));
|
|
29
|
+
}
|
|
12
30
|
|
|
13
31
|
function normalizeName(name) {
|
|
14
32
|
return String(name || "").toLowerCase();
|
|
@@ -114,6 +132,7 @@ class BrowserSessionStore {
|
|
|
114
132
|
browserEpoch: identity.browserEpoch,
|
|
115
133
|
createdAt: timestamp,
|
|
116
134
|
updatedAt: timestamp,
|
|
135
|
+
lastAccessedAt: values.lastAccessedAt || timestamp,
|
|
117
136
|
lastValidatedAt: values.lastValidatedAt || timestamp,
|
|
118
137
|
};
|
|
119
138
|
bucket.sessions[key] = record;
|
|
@@ -137,6 +156,7 @@ class BrowserSessionStore {
|
|
|
137
156
|
browserEpoch: identity.browserEpoch,
|
|
138
157
|
createdAt: existing?.createdAt || timestamp,
|
|
139
158
|
updatedAt: timestamp,
|
|
159
|
+
lastAccessedAt: values.lastAccessedAt || timestamp,
|
|
140
160
|
lastValidatedAt: values.lastValidatedAt || timestamp,
|
|
141
161
|
};
|
|
142
162
|
delete record.invalidReason;
|
|
@@ -149,7 +169,13 @@ class BrowserSessionStore {
|
|
|
149
169
|
update(identity, name, patch) {
|
|
150
170
|
const existing = this.get(identity, name);
|
|
151
171
|
if (!existing) throw surfError("session_unknown", `unknown session: ${name}`, { session: name });
|
|
152
|
-
|
|
172
|
+
const timestamp = this.now();
|
|
173
|
+
return this.replace(identity, name, {
|
|
174
|
+
...existing,
|
|
175
|
+
...patch,
|
|
176
|
+
updatedAt: timestamp,
|
|
177
|
+
lastAccessedAt: patch?.lastAccessedAt || timestamp,
|
|
178
|
+
});
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
remove(identity, name) {
|
|
@@ -212,7 +238,8 @@ class BrowserSessionStore {
|
|
|
212
238
|
let changed = false;
|
|
213
239
|
for (const record of Object.values(bucket.sessions || {})) {
|
|
214
240
|
if (record.tabId !== tabId) continue;
|
|
215
|
-
|
|
241
|
+
const timestamp = this.now();
|
|
242
|
+
Object.assign(record, patch, { updatedAt: timestamp, lastAccessedAt: patch?.lastAccessedAt || timestamp });
|
|
216
243
|
changed = true;
|
|
217
244
|
}
|
|
218
245
|
if (changed) this.save(state);
|
|
@@ -266,6 +293,7 @@ module.exports = {
|
|
|
266
293
|
BrowserSessionStore,
|
|
267
294
|
SESSION_NAME_PATTERN,
|
|
268
295
|
STORE_VERSION,
|
|
296
|
+
parseDurationMs,
|
|
269
297
|
normalizeName,
|
|
270
298
|
validateSessionName,
|
|
271
299
|
};
|
|
@@ -1,11 +1,19 @@
|
|
|
1
|
-
const CHATGPT_EFFORT_CHOICES = ["
|
|
1
|
+
const CHATGPT_EFFORT_CHOICES = ["instant", "medium", "high", "xhigh", "pro"];
|
|
2
|
+
const CHATGPT_EFFORT_VALUE = new Map([
|
|
3
|
+
["instant", 0],
|
|
4
|
+
["medium", 1],
|
|
5
|
+
["high", 2],
|
|
6
|
+
["xhigh", 3],
|
|
7
|
+
["pro", 4],
|
|
8
|
+
]);
|
|
9
|
+
|
|
2
10
|
const CHATGPT_MODEL_ALIASES = new Map([
|
|
3
|
-
["
|
|
4
|
-
["
|
|
5
|
-
["
|
|
6
|
-
["
|
|
7
|
-
["
|
|
8
|
-
["
|
|
11
|
+
["latest", "latest"],
|
|
12
|
+
["gpt6", "gpt6astra"],
|
|
13
|
+
["chatgpt6", "gpt6astra"],
|
|
14
|
+
["gpt6astra", "gpt6astra"],
|
|
15
|
+
["chatgpt6astra", "gpt6astra"],
|
|
16
|
+
["6", "gpt6astra"],
|
|
9
17
|
["55", "gpt55"],
|
|
10
18
|
["gpt55", "gpt55"],
|
|
11
19
|
["chatgpt55", "gpt55"],
|
|
@@ -14,6 +22,15 @@ const CHATGPT_MODEL_ALIASES = new Map([
|
|
|
14
22
|
["chatgpt56sol", "gpt56sol"],
|
|
15
23
|
]);
|
|
16
24
|
|
|
25
|
+
const CHATGPT_EFFORT_ALIASES = new Map([
|
|
26
|
+
["instant", "instant"],
|
|
27
|
+
["medium", "medium"],
|
|
28
|
+
["high", "high"],
|
|
29
|
+
["xhigh", "xhigh"],
|
|
30
|
+
["extrahigh", "xhigh"],
|
|
31
|
+
["pro", "pro"],
|
|
32
|
+
]);
|
|
33
|
+
|
|
17
34
|
function normalizeChatGPTModelChoice(desiredModel) {
|
|
18
35
|
const normalized = String(desiredModel || "")
|
|
19
36
|
.toLowerCase()
|
|
@@ -23,43 +40,68 @@ function normalizeChatGPTModelChoice(desiredModel) {
|
|
|
23
40
|
}
|
|
24
41
|
|
|
25
42
|
function normalizeChatGPTEffortChoice(desiredEffort) {
|
|
26
|
-
const normalized = String(desiredEffort || "")
|
|
27
|
-
|
|
43
|
+
const normalized = String(desiredEffort || "")
|
|
44
|
+
.toLowerCase()
|
|
45
|
+
.replace(/[^a-z0-9]/g, "");
|
|
46
|
+
return CHATGPT_EFFORT_ALIASES.get(normalized) || null;
|
|
28
47
|
}
|
|
29
48
|
|
|
30
49
|
function normalizedWords(value) {
|
|
31
50
|
return String(value || "")
|
|
32
51
|
.toLowerCase()
|
|
33
|
-
.replace(/[^a-z0-9]+/g, " ")
|
|
52
|
+
.replace(/[^a-z0-9.]+/g, " ")
|
|
34
53
|
.trim()
|
|
35
54
|
.split(/\s+/)
|
|
36
55
|
.filter(Boolean);
|
|
37
56
|
}
|
|
38
57
|
|
|
58
|
+
function normalizedText(value) {
|
|
59
|
+
return normalizedWords(value).join(" ");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function modelKeyFromValue(value) {
|
|
63
|
+
const text = normalizedText(value);
|
|
64
|
+
if (!text) return null;
|
|
65
|
+
if (/^latest$/.test(text)) return "latest";
|
|
66
|
+
if (/\b5\.6\b/.test(text) && /\bsol\b/.test(text)) return "gpt56sol";
|
|
67
|
+
if (/\b5\.5\b/.test(text)) return "gpt55";
|
|
68
|
+
if (/\bgpt\s*6\b/.test(text) || /\bchatgpt\s*6\b/.test(text) || /^6(?:\s|$)/.test(text)) {
|
|
69
|
+
return "gpt6astra";
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
39
74
|
function modelCandidateMatches(item, targetModel) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (targetModel.startsWith("gpt") && normalizedValue.includes(targetModel)) return true;
|
|
45
|
-
const variants = ["instant", "thinking", "pro"].filter((variant) =>
|
|
46
|
-
normalizedWords(value).includes(variant),
|
|
47
|
-
);
|
|
48
|
-
return variants.length === 1 && variants[0] === targetModel;
|
|
49
|
-
});
|
|
75
|
+
if (item?.modelKey === targetModel) return true;
|
|
76
|
+
const values = [item?.label, item?.displayLabel, item?.testId?.replace(/^model-switcher-/, "")]
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
return values.some((value) => modelKeyFromValue(value) === targetModel);
|
|
50
79
|
}
|
|
51
80
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (labelVariants.size > 0) {
|
|
57
|
-
return labelVariants.size === 1 && labelVariants.has(targetEffort);
|
|
81
|
+
function modelSelectableCandidateMatches(item, targetModel) {
|
|
82
|
+
if (modelCandidateMatches(item, targetModel)) return true;
|
|
83
|
+
if (targetModel === "gpt6astra") {
|
|
84
|
+
return [item?.label, item?.displayLabel].filter(Boolean).some((value) => modelKeyFromValue(value) === "latest");
|
|
58
85
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function effortKeyFromValue(value) {
|
|
90
|
+
const text = normalizedText(value);
|
|
91
|
+
if (!text) return null;
|
|
92
|
+
if (/\bextra\s+high\b/.test(text) || /\bxhigh\b/.test(text)) return "xhigh";
|
|
93
|
+
if (/\binstant\b/.test(text)) return "instant";
|
|
94
|
+
if (/\bmedium\b/.test(text)) return "medium";
|
|
95
|
+
if (/\bhigh\b/.test(text)) return "high";
|
|
96
|
+
if (/\bpro\b/.test(text)) return "pro";
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function effortCandidateMatches(item, targetEffort) {
|
|
101
|
+
const labelKey = item?.effortKey || effortKeyFromValue(item?.label || item?.displayLabel);
|
|
102
|
+
if (!labelKey || labelKey !== targetEffort) return false;
|
|
103
|
+
if (item?.value === undefined || item?.value === null) return true;
|
|
104
|
+
return Number(item.value) === CHATGPT_EFFORT_VALUE.get(targetEffort);
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
function uniqueMatch(items, matches) {
|
|
@@ -75,7 +117,7 @@ function resolveChatGPTModelMenuOption(items, desiredModel) {
|
|
|
75
117
|
items,
|
|
76
118
|
(item) =>
|
|
77
119
|
["button", "menuitem", "menuitemradio", "radio"].includes(item?.role) &&
|
|
78
|
-
|
|
120
|
+
modelSelectableCandidateMatches(item, targetModel),
|
|
79
121
|
);
|
|
80
122
|
}
|
|
81
123
|
|
|
@@ -85,7 +127,8 @@ function verifyChatGPTModelSelection(items, desiredModel) {
|
|
|
85
127
|
return uniqueMatch(
|
|
86
128
|
items,
|
|
87
129
|
(item) =>
|
|
88
|
-
(
|
|
130
|
+
(!["menuitemradio", "radio"].includes(item?.role) || item.selected === true) &&
|
|
131
|
+
(typeof item?.label === "string" || typeof item?.displayLabel === "string" || typeof item?.testId === "string") &&
|
|
89
132
|
modelCandidateMatches(item, targetModel),
|
|
90
133
|
);
|
|
91
134
|
}
|
|
@@ -96,7 +139,7 @@ function resolveChatGPTEffortMenuOption(items, desiredEffort) {
|
|
|
96
139
|
return uniqueMatch(
|
|
97
140
|
items,
|
|
98
141
|
(item) =>
|
|
99
|
-
["button", "menuitem", "menuitemradio"].includes(item?.role) &&
|
|
142
|
+
["slider", "button", "menuitem", "menuitemradio"].includes(item?.role) &&
|
|
100
143
|
effortCandidateMatches(item, targetEffort),
|
|
101
144
|
);
|
|
102
145
|
}
|
|
@@ -107,7 +150,7 @@ function verifyChatGPTEffortSelection(items, desiredEffort) {
|
|
|
107
150
|
return uniqueMatch(
|
|
108
151
|
items,
|
|
109
152
|
(item) =>
|
|
110
|
-
(typeof item?.label === "string" || typeof item?.testId === "string") &&
|
|
153
|
+
(typeof item?.label === "string" || typeof item?.displayLabel === "string" || typeof item?.testId === "string") &&
|
|
111
154
|
effortCandidateMatches(item, targetEffort),
|
|
112
155
|
);
|
|
113
156
|
}
|
|
@@ -115,7 +158,7 @@ function verifyChatGPTEffortSelection(items, desiredEffort) {
|
|
|
115
158
|
function boundedOptionLabels(items) {
|
|
116
159
|
if (!Array.isArray(items)) return [];
|
|
117
160
|
return items
|
|
118
|
-
.map((item) => String(item?.label || "").replace(/\s+/g, " ").trim().slice(0, 80))
|
|
161
|
+
.map((item) => String(item?.displayLabel || item?.label || "").replace(/\s+/g, " ").trim().slice(0, 80))
|
|
119
162
|
.filter(Boolean)
|
|
120
163
|
.filter((label, index, labels) => labels.indexOf(label) === index)
|
|
121
164
|
.slice(0, 10);
|
|
@@ -123,6 +166,7 @@ function boundedOptionLabels(items) {
|
|
|
123
166
|
|
|
124
167
|
module.exports = {
|
|
125
168
|
CHATGPT_EFFORT_CHOICES,
|
|
169
|
+
CHATGPT_EFFORT_VALUE,
|
|
126
170
|
boundedOptionLabels,
|
|
127
171
|
normalizeChatGPTEffortChoice,
|
|
128
172
|
normalizeChatGPTModelChoice,
|