codex-chatgpt-control 0.5.0-alpha.1 → 0.5.1-alpha.2
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/CHANGELOG.md +22 -0
- package/contracts/v1/fixtures/backend-capabilities.json +1 -0
- package/contracts/v1/fixtures/command-descriptors.json +29 -2
- package/contracts/v1/fixtures/doctor-bridge-upload.json +2 -2
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +26 -4
- package/contracts/v1/fixtures/files-attach-indeterminate.json +30 -0
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/messages-stop-indeterminate.json +31 -0
- package/contracts/v1/fixtures/messages-stop-needs-confirmation.json +20 -0
- package/contracts/v1/fixtures/messages-stop-noop.json +19 -0
- package/contracts/v1/fixtures/messages-stop-success.json +21 -0
- package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +3 -3
- package/contracts/v1/fixtures/reports-create-redacted.json +3 -3
- package/contracts/v1/fixtures/reports-summarize-redacted.json +1 -1
- package/contracts/v1/fixtures/run-browser-bridge-blocker.json +12 -12
- package/contracts/v1/fixtures/stream-blocked.ndjson +2 -2
- package/contracts/v1/manifest.json +25 -0
- package/contracts/v1/parity-suite.json +19 -1
- package/dist/codex-chatgpt-control-backend.mjs +3654 -603
- package/dist/codex-chatgpt-control.bundle.mjs +3627 -546
- package/dist/src/backend/client.d.ts +2 -1
- package/dist/src/backend/client.js +1 -0
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +1 -0
- package/dist/src/backend/session.js +2 -0
- package/dist/src/browser/attach.d.ts +1 -0
- package/dist/src/browser/attach.js +69 -35
- package/dist/src/browser/chatgpt-url.d.ts +8 -0
- package/dist/src/browser/chatgpt-url.js +28 -0
- package/dist/src/browser/downloads.d.ts +1 -1
- package/dist/src/browser/downloads.js +13 -4
- package/dist/src/browser/page-state.js +211 -4
- package/dist/src/client.d.ts +2 -1
- package/dist/src/client.js +2 -1
- package/dist/src/commands/configuration.js +330 -73
- package/dist/src/commands/context.d.ts +5 -1
- package/dist/src/commands/context.js +2 -2
- package/dist/src/commands/conversation.js +13 -1
- package/dist/src/commands/doctor.js +24 -2
- package/dist/src/commands/experience.d.ts +1 -0
- package/dist/src/commands/experience.js +156 -23
- package/dist/src/commands/files.d.ts +1 -0
- package/dist/src/commands/files.js +877 -170
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +310 -2
- package/dist/src/commands/modes.js +197 -52
- package/dist/src/commands/registry.js +8 -0
- package/dist/src/commands/sequence.js +3 -1
- package/dist/src/commands/session.d.ts +4 -1
- package/dist/src/commands/session.js +34 -3
- package/dist/src/dom/generation-state.d.ts +6 -1
- package/dist/src/dom/generation-state.js +206 -45
- package/dist/src/dom/locale/am.d.ts +23 -1
- package/dist/src/dom/locale/am.js +23 -1
- package/dist/src/dom/locale/ar.d.ts +21 -1
- package/dist/src/dom/locale/ar.js +24 -4
- package/dist/src/dom/locale/bg.d.ts +19 -1
- package/dist/src/dom/locale/bg.js +22 -4
- package/dist/src/dom/locale/bn.d.ts +23 -1
- package/dist/src/dom/locale/bn.js +23 -1
- package/dist/src/dom/locale/bs.d.ts +17 -1
- package/dist/src/dom/locale/bs.js +21 -5
- package/dist/src/dom/locale/ca.d.ts +20 -1
- package/dist/src/dom/locale/ca.js +25 -6
- package/dist/src/dom/locale/cs.d.ts +17 -1
- package/dist/src/dom/locale/cs.js +17 -1
- package/dist/src/dom/locale/da.d.ts +17 -1
- package/dist/src/dom/locale/da.js +18 -2
- package/dist/src/dom/locale/de.d.ts +16 -1
- package/dist/src/dom/locale/de.js +18 -3
- package/dist/src/dom/locale/el.d.ts +18 -1
- package/dist/src/dom/locale/el.js +21 -4
- package/dist/src/dom/locale/en.d.ts +2 -1
- package/dist/src/dom/locale/en.js +2 -1
- package/dist/src/dom/locale/es-419.d.ts +18 -1
- package/dist/src/dom/locale/es-419.js +18 -1
- package/dist/src/dom/locale/es-ES.d.ts +18 -1
- package/dist/src/dom/locale/es-ES.js +18 -1
- package/dist/src/dom/locale/et.d.ts +16 -1
- package/dist/src/dom/locale/et.js +16 -1
- package/dist/src/dom/locale/fa.d.ts +21 -1
- package/dist/src/dom/locale/fa.js +23 -3
- package/dist/src/dom/locale/fi.d.ts +18 -1
- package/dist/src/dom/locale/fi.js +18 -1
- package/dist/src/dom/locale/fr-CA.d.ts +15 -1
- package/dist/src/dom/locale/fr-CA.js +15 -1
- package/dist/src/dom/locale/fr-FR.d.ts +18 -1
- package/dist/src/dom/locale/fr-FR.js +22 -5
- package/dist/src/dom/locale/gu.d.ts +23 -1
- package/dist/src/dom/locale/gu.js +27 -5
- package/dist/src/dom/locale/hi.d.ts +23 -1
- package/dist/src/dom/locale/hi.js +25 -3
- package/dist/src/dom/locale/hr.d.ts +16 -1
- package/dist/src/dom/locale/hr.js +19 -4
- package/dist/src/dom/locale/hu.d.ts +17 -1
- package/dist/src/dom/locale/hu.js +20 -4
- package/dist/src/dom/locale/hy.d.ts +23 -1
- package/dist/src/dom/locale/hy.js +26 -4
- package/dist/src/dom/locale/id.d.ts +17 -1
- package/dist/src/dom/locale/id.js +19 -3
- package/dist/src/dom/locale/index.js +1 -0
- package/dist/src/dom/locale/is.d.ts +21 -1
- package/dist/src/dom/locale/is.js +21 -1
- package/dist/src/dom/locale/it.d.ts +15 -1
- package/dist/src/dom/locale/it.js +20 -6
- package/dist/src/dom/locale/ja.d.ts +19 -1
- package/dist/src/dom/locale/ja.js +23 -5
- package/dist/src/dom/locale/ka.d.ts +23 -1
- package/dist/src/dom/locale/ka.js +23 -1
- package/dist/src/dom/locale/kk.d.ts +23 -1
- package/dist/src/dom/locale/kk.js +25 -3
- package/dist/src/dom/locale/kn.d.ts +22 -1
- package/dist/src/dom/locale/kn.js +26 -5
- package/dist/src/dom/locale/ko.d.ts +17 -1
- package/dist/src/dom/locale/ko.js +17 -1
- package/dist/src/dom/locale/lt.d.ts +18 -1
- package/dist/src/dom/locale/lt.js +20 -3
- package/dist/src/dom/locale/lv.d.ts +18 -1
- package/dist/src/dom/locale/lv.js +18 -1
- package/dist/src/dom/locale/mk.d.ts +22 -1
- package/dist/src/dom/locale/mk.js +26 -5
- package/dist/src/dom/locale/ml.d.ts +23 -1
- package/dist/src/dom/locale/ml.js +25 -3
- package/dist/src/dom/locale/mn.d.ts +23 -1
- package/dist/src/dom/locale/mn.js +23 -1
- package/dist/src/dom/locale/mr.d.ts +23 -1
- package/dist/src/dom/locale/mr.js +25 -3
- package/dist/src/dom/locale/ms.d.ts +20 -1
- package/dist/src/dom/locale/ms.js +20 -1
- package/dist/src/dom/locale/my.d.ts +22 -1
- package/dist/src/dom/locale/my.js +24 -3
- package/dist/src/dom/locale/nb.d.ts +17 -1
- package/dist/src/dom/locale/nb.js +17 -1
- package/dist/src/dom/locale/nl.d.ts +20 -2
- package/dist/src/dom/locale/nl.js +22 -4
- package/dist/src/dom/locale/pa.d.ts +23 -1
- package/dist/src/dom/locale/pa.js +27 -5
- package/dist/src/dom/locale/pl.d.ts +17 -1
- package/dist/src/dom/locale/pl.js +19 -3
- package/dist/src/dom/locale/pt-BR.d.ts +18 -1
- package/dist/src/dom/locale/pt-BR.js +20 -3
- package/dist/src/dom/locale/pt-PT.d.ts +18 -1
- package/dist/src/dom/locale/pt-PT.js +22 -5
- package/dist/src/dom/locale/ro.d.ts +15 -1
- package/dist/src/dom/locale/ro.js +18 -4
- package/dist/src/dom/locale/ru.d.ts +23 -1
- package/dist/src/dom/locale/ru.js +24 -2
- package/dist/src/dom/locale/sk.d.ts +17 -1
- package/dist/src/dom/locale/sk.js +19 -3
- package/dist/src/dom/locale/sl.d.ts +21 -1
- package/dist/src/dom/locale/sl.js +24 -4
- package/dist/src/dom/locale/so.d.ts +22 -1
- package/dist/src/dom/locale/so.js +24 -3
- package/dist/src/dom/locale/sq.d.ts +21 -1
- package/dist/src/dom/locale/sq.js +25 -5
- package/dist/src/dom/locale/sr.d.ts +24 -1
- package/dist/src/dom/locale/sr.js +25 -2
- package/dist/src/dom/locale/sv.d.ts +16 -1
- package/dist/src/dom/locale/sv.js +16 -1
- package/dist/src/dom/locale/sw.d.ts +18 -1
- package/dist/src/dom/locale/sw.js +18 -1
- package/dist/src/dom/locale/ta.d.ts +23 -1
- package/dist/src/dom/locale/ta.js +27 -5
- package/dist/src/dom/locale/te.d.ts +23 -1
- package/dist/src/dom/locale/te.js +27 -5
- package/dist/src/dom/locale/th.d.ts +11 -1
- package/dist/src/dom/locale/th.js +11 -1
- package/dist/src/dom/locale/tl.d.ts +8 -1
- package/dist/src/dom/locale/tl.js +8 -1
- package/dist/src/dom/locale/tr.d.ts +21 -1
- package/dist/src/dom/locale/tr.js +23 -3
- package/dist/src/dom/locale/types.d.ts +1 -1
- package/dist/src/dom/locale/uk.d.ts +23 -1
- package/dist/src/dom/locale/uk.js +25 -3
- package/dist/src/dom/locale/ur.d.ts +22 -1
- package/dist/src/dom/locale/ur.js +25 -4
- package/dist/src/dom/locale/vi.d.ts +22 -1
- package/dist/src/dom/locale/vi.js +25 -4
- package/dist/src/dom/locale/zh-HK.d.ts +21 -1
- package/dist/src/dom/locale/zh-HK.js +23 -3
- package/dist/src/dom/locale/zh-Hans.d.ts +23 -1
- package/dist/src/dom/locale/zh-Hans.js +27 -5
- package/dist/src/dom/locale/zh-TW.d.ts +23 -1
- package/dist/src/dom/locale/zh-TW.js +25 -3
- package/dist/src/dom/menus.js +26 -2
- package/dist/src/dom/selectors.d.ts +1 -0
- package/dist/src/dom/selectors.js +9 -1
- package/dist/src/dom/wait-snapshot.d.ts +1 -0
- package/dist/src/dom/wait-snapshot.js +45 -22
- package/dist/src/errors.d.ts +1 -1
- package/dist/src/errors.js +2 -2
- package/dist/src/platform/local-paths.d.ts +1 -0
- package/dist/src/platform/local-paths.js +7 -3
- package/dist/src/safety/risk.d.ts +1 -0
- package/dist/src/safety/risk.js +1 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +10 -1
- package/dist/src/scripts/apply-intelligence-locale-captures.js +219 -7
- package/dist/src/scripts/capture-intelligence-locales.d.ts +119 -1
- package/dist/src/scripts/capture-intelligence-locales.js +653 -232
- package/dist/src/scripts/capture-surface-profile.d.ts +3 -1
- package/dist/src/scripts/capture-surface-profile.js +75 -25
- package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
- package/dist/src/scripts/live-smoke/scenarios.js +321 -13
- package/dist/src/scripts/live-smoke.js +1 -0
- package/dist/src/scripts/locale-capture/surface-graph.d.ts +31 -0
- package/dist/src/scripts/locale-capture/surface-graph.js +50 -0
- package/dist/src/scripts/release-canary-module.d.ts +20 -0
- package/dist/src/scripts/release-canary-module.js +86 -0
- package/dist/src/types.d.ts +30 -6
- package/package.json +8 -2
- package/references/2026-07-16-chat-work-surfaces.md +13 -0
- package/references/2026-08-08-model-selection-locale-sweep.md +32 -0
- package/references/backend-protocol.md +22 -7
- package/references/language-coverage.md +24 -13
- package/references/localization.md +36 -14
- package/references/python-parity.md +9 -3
- package/references/troubleshooting.md +26 -4
|
@@ -4,8 +4,8 @@ import { dirname, resolve } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { attachChatGPTBrowser } from "../browser/attach.js";
|
|
6
6
|
import { BROWSER_BRIDGE_REMEDIATION, BROWSER_BRIDGE_UNAVAILABLE_MESSAGE } from "../errors.js";
|
|
7
|
-
import { localeLabels } from "../dom/locale-labels.js";
|
|
8
7
|
import { nonEnglishLanguages, readLanguageCoverage } from "./locale-capture/language-coverage.js";
|
|
8
|
+
import { assignOrderedChatConfigurationRows, assignChatSelectedSurfaceOptions, assignOrderedSurfaceOptions, assignOrderedWorkConfigurationRows, } from "./locale-capture/surface-graph.js";
|
|
9
9
|
const SCHEMA_VERSION = "chatgpt.browser_control.intelligence_locale_capture.v1";
|
|
10
10
|
const CHATGPT_HOME = "https://chatgpt.com/";
|
|
11
11
|
const DEFAULT_SWITCH_TIMEOUT_MS = 15_000;
|
|
@@ -45,6 +45,8 @@ const USAGE = [
|
|
|
45
45
|
" --no-open-version-submenu Do not open the model-version submenu.",
|
|
46
46
|
" --capture-generation-state Submit one bounded probe per locale to capture localized running/stopped generation labels. Default: false.",
|
|
47
47
|
" --no-capture-generation-state Disable generation-state capture.",
|
|
48
|
+
" --capture-surfaces Capture Chat/Work radios, composers, Power/Advanced controls, and ordered configuration rows.",
|
|
49
|
+
" --no-capture-surfaces Disable Chat/Work surface capture. Default.",
|
|
48
50
|
" --generation-prompt Override the redacted probe prompt used only for generation-state capture.",
|
|
49
51
|
" --generation-timeout-ms Wait for generation controls after submit. Default: 8000.",
|
|
50
52
|
" --restore Restore the initially selected language after a sweep. Default with --auto-switch.",
|
|
@@ -68,6 +70,7 @@ export async function main(argv = process.argv.slice(2), runtime = globalThis) {
|
|
|
68
70
|
throw error;
|
|
69
71
|
}
|
|
70
72
|
const languages = await readLanguageCoverage(options.coveragePath);
|
|
73
|
+
const knownLanguageNames = languages.map(language => language.nativeName);
|
|
71
74
|
if (options.printQueue) {
|
|
72
75
|
printQueue(nonEnglishLanguages(languages));
|
|
73
76
|
return 0;
|
|
@@ -85,15 +88,16 @@ export async function main(argv = process.argv.slice(2), runtime = globalThis) {
|
|
|
85
88
|
}, null, 2));
|
|
86
89
|
return 2;
|
|
87
90
|
}
|
|
88
|
-
const initialLanguage = options.autoSwitch
|
|
89
|
-
? await
|
|
91
|
+
const initialLanguage = options.autoSwitch && options.restore
|
|
92
|
+
? await readInitialLanguage(runtime, options, knownLanguageNames)
|
|
90
93
|
: undefined;
|
|
91
94
|
const sweepLanguages = resolveSweepLanguages(options, languages);
|
|
92
95
|
const records = [];
|
|
96
|
+
let restoreFailed = false;
|
|
93
97
|
try {
|
|
94
98
|
for (const language of sweepLanguages) {
|
|
95
99
|
const page = await attachCapturePage(runtime, options);
|
|
96
|
-
const record = await captureOne(page, language, options);
|
|
100
|
+
const record = await captureOne(page, language, options, knownLanguageNames);
|
|
97
101
|
records.push(record);
|
|
98
102
|
await appendRecord(options.out, record);
|
|
99
103
|
printCaptureRecord(record, options.out);
|
|
@@ -106,12 +110,36 @@ export async function main(argv = process.argv.slice(2), runtime = globalThis) {
|
|
|
106
110
|
}
|
|
107
111
|
finally {
|
|
108
112
|
if (options.autoSwitch && options.restore && initialLanguage !== undefined) {
|
|
109
|
-
await attachCapturePage(runtime, options).then(page => restoreLanguage(page, initialLanguage, options)).catch(error => {
|
|
110
|
-
|
|
113
|
+
await attachCapturePage(runtime, options).then(page => restoreLanguage(page, initialLanguage.selectedLabel, initialLanguage.htmlLang, knownLanguageNames, options)).catch(error => {
|
|
114
|
+
restoreFailed = true;
|
|
115
|
+
console.error(`Unable to restore initial language ${initialLanguage.selectedLabel}: ${error instanceof Error ? error.message : String(error)}`);
|
|
111
116
|
});
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
|
-
return records.some(record => record.status === "blocked"
|
|
119
|
+
return restoreFailed || records.some(record => record.status === "blocked"
|
|
120
|
+
|| !surfaceCaptureSucceeded(options.captureSurfaces, record.surfaceCapture)) ? 1 : 0;
|
|
121
|
+
}
|
|
122
|
+
async function readInitialLanguage(runtime, options, knownLanguageNames) {
|
|
123
|
+
let lastError;
|
|
124
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
125
|
+
try {
|
|
126
|
+
const page = await attachCapturePage(runtime, options);
|
|
127
|
+
const proof = await renderedProof(page);
|
|
128
|
+
const selected = await readSelectedLanguage(page, knownLanguageNames);
|
|
129
|
+
if (selected !== undefined && proof.htmlLang !== undefined && proof.htmlLang.length > 0) {
|
|
130
|
+
return { selectedLabel: selected, htmlLang: proof.htmlLang };
|
|
131
|
+
}
|
|
132
|
+
lastError = new Error("Settings language value was empty.");
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
lastError = error;
|
|
136
|
+
}
|
|
137
|
+
await wait(500);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`Unable to establish the initial ChatGPT language before a restorable sweep: ${lastError instanceof Error ? lastError.message : String(lastError)}`);
|
|
140
|
+
}
|
|
141
|
+
export function surfaceCaptureSucceeded(requested, capture) {
|
|
142
|
+
return !requested || (capture?.status === "ok" && capture.restoredChat);
|
|
115
143
|
}
|
|
116
144
|
export function parseArgs(argv) {
|
|
117
145
|
let locale;
|
|
@@ -124,6 +152,7 @@ export function parseArgs(argv) {
|
|
|
124
152
|
let locales;
|
|
125
153
|
let openVersionSubmenu = true;
|
|
126
154
|
let captureGenerationState = false;
|
|
155
|
+
let captureSurfaces = false;
|
|
127
156
|
let generationPrompt = DEFAULT_GENERATION_PROMPT;
|
|
128
157
|
let generationCaptureTimeoutMs = DEFAULT_GENERATION_CAPTURE_TIMEOUT_MS;
|
|
129
158
|
let restore;
|
|
@@ -176,6 +205,12 @@ export function parseArgs(argv) {
|
|
|
176
205
|
case "--no-capture-generation-state":
|
|
177
206
|
captureGenerationState = false;
|
|
178
207
|
break;
|
|
208
|
+
case "--capture-surfaces":
|
|
209
|
+
captureSurfaces = true;
|
|
210
|
+
break;
|
|
211
|
+
case "--no-capture-surfaces":
|
|
212
|
+
captureSurfaces = false;
|
|
213
|
+
break;
|
|
179
214
|
case "--generation-prompt":
|
|
180
215
|
generationPrompt = requiredValue(argv, ++index, arg);
|
|
181
216
|
break;
|
|
@@ -222,6 +257,7 @@ export function parseArgs(argv) {
|
|
|
222
257
|
locales,
|
|
223
258
|
openVersionSubmenu,
|
|
224
259
|
captureGenerationState,
|
|
260
|
+
captureSurfaces,
|
|
225
261
|
generationPrompt,
|
|
226
262
|
generationCaptureTimeoutMs,
|
|
227
263
|
restore: restore ?? autoSwitch,
|
|
@@ -276,11 +312,11 @@ function resolveSweepLanguages(options, languages) {
|
|
|
276
312
|
status: ""
|
|
277
313
|
}];
|
|
278
314
|
}
|
|
279
|
-
async function captureOne(page, language, options) {
|
|
315
|
+
export async function captureOne(page, language, options, knownLanguageNames, dependencies = {}) {
|
|
280
316
|
const warnings = [];
|
|
281
317
|
try {
|
|
282
318
|
if (options.autoSwitch) {
|
|
283
|
-
await switchLanguage(page, language, options);
|
|
319
|
+
await switchLanguage(page, language, knownLanguageNames, options);
|
|
284
320
|
const proof = await renderedProof(page);
|
|
285
321
|
if (!htmlLangMatches(proof.htmlLang, language.bcp47)) {
|
|
286
322
|
return blockedRecord(language, proof, warnings, "rendered_locale_mismatch", `Rendered html lang ${proof.htmlLang || "unknown"} did not match requested ${language.bcp47}.`);
|
|
@@ -288,9 +324,16 @@ async function captureOne(page, language, options) {
|
|
|
288
324
|
}
|
|
289
325
|
await closeSettingsIfOpen(page);
|
|
290
326
|
await returnToChatSurface(page, options);
|
|
291
|
-
|
|
327
|
+
if (options.captureSurfaces) {
|
|
328
|
+
await ensureChatSurfaceSelected(page, options);
|
|
329
|
+
}
|
|
330
|
+
const picker = await (dependencies.captureIntelligencePicker ?? captureIntelligencePicker)(page, options);
|
|
331
|
+
await closeFloatingMenus(page);
|
|
332
|
+
const surfaceCapture = options.captureSurfaces
|
|
333
|
+
? await captureLocaleSurface(page, options, picker.configuration)
|
|
334
|
+
: undefined;
|
|
292
335
|
const generation = options.captureGenerationState
|
|
293
|
-
? await captureGenerationStateLabels(page, options)
|
|
336
|
+
? await (dependencies.captureGenerationStateLabels ?? captureGenerationStateLabels)(page, options)
|
|
294
337
|
: undefined;
|
|
295
338
|
if (generation !== undefined) {
|
|
296
339
|
warnings.push(...generation.warnings);
|
|
@@ -310,6 +353,10 @@ async function captureOne(page, language, options) {
|
|
|
310
353
|
modelVersionLabels: picker.modelVersionLabels,
|
|
311
354
|
warnings
|
|
312
355
|
};
|
|
356
|
+
if (surfaceCapture !== undefined) {
|
|
357
|
+
record.surfaceCapture = surfaceCapture;
|
|
358
|
+
warnings.push(...surfaceCapture.warnings);
|
|
359
|
+
}
|
|
313
360
|
if (generation !== undefined) {
|
|
314
361
|
record.generationStopLabels = generation.stopLabels;
|
|
315
362
|
record.generationStoppedLabels = generation.stoppedLabels;
|
|
@@ -322,39 +369,86 @@ async function captureOne(page, language, options) {
|
|
|
322
369
|
return blockedRecord(language, proof, warnings, "capture_failed", error instanceof Error ? error.message : String(error));
|
|
323
370
|
}
|
|
324
371
|
}
|
|
325
|
-
async function switchLanguage(page, language, options) {
|
|
372
|
+
async function switchLanguage(page, language, knownLanguageNames, options) {
|
|
326
373
|
await openSettings(page, options);
|
|
327
|
-
await openLanguageCombobox(page);
|
|
374
|
+
await openLanguageCombobox(page, knownLanguageNames);
|
|
328
375
|
await clickOptionExact(page, language.nativeName);
|
|
329
|
-
await
|
|
376
|
+
await wait(options.settleMs);
|
|
377
|
+
if (page.goto !== undefined) {
|
|
378
|
+
await page.goto(CHATGPT_HOME).catch(error => {
|
|
379
|
+
if (!/ERR_ABORTED/i.test(error instanceof Error ? error.message : String(error)))
|
|
380
|
+
throw error;
|
|
381
|
+
});
|
|
330
382
|
await wait(options.settleMs);
|
|
331
|
-
}
|
|
383
|
+
}
|
|
384
|
+
await waitForRenderedLanguage(page, language.bcp47, options.switchTimeoutMs);
|
|
332
385
|
}
|
|
333
|
-
async function restoreLanguage(page, selectedLanguageText, options) {
|
|
386
|
+
async function restoreLanguage(page, selectedLanguageText, expectedHtmlLang, knownLanguageNames, options) {
|
|
334
387
|
await openSettings(page, options);
|
|
335
|
-
await openLanguageCombobox(page);
|
|
388
|
+
await openLanguageCombobox(page, knownLanguageNames);
|
|
336
389
|
await clickOptionExact(page, selectedLanguageText);
|
|
337
390
|
await wait(options.settleMs);
|
|
338
|
-
|
|
391
|
+
let restored = false;
|
|
392
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
393
|
+
if (page.goto !== undefined) {
|
|
394
|
+
await page.goto(CHATGPT_HOME).catch(error => {
|
|
395
|
+
if (!/ERR_ABORTED/i.test(error instanceof Error ? error.message : String(error)))
|
|
396
|
+
throw error;
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
await wait(options.settleMs);
|
|
400
|
+
try {
|
|
401
|
+
await waitForRenderedLanguage(page, expectedHtmlLang, options.switchTimeoutMs);
|
|
402
|
+
restored = true;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (attempt === 1)
|
|
407
|
+
throw error;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (!restored)
|
|
411
|
+
throw new Error(`Rendered language did not restore to ${expectedHtmlLang}.`);
|
|
412
|
+
for (let attempt = 0; attempt < 6; attempt += 1) {
|
|
413
|
+
await closeSettingsIfOpen(page);
|
|
414
|
+
await wait(500);
|
|
415
|
+
if (!await isSettingsOpen(page))
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
throw new Error("Settings modal remained open after restoring the initial language.");
|
|
339
419
|
}
|
|
340
420
|
async function openSettings(page, options) {
|
|
341
421
|
if (await isSettingsOpen(page))
|
|
342
422
|
return;
|
|
343
423
|
await closeFloatingMenus(page);
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
424
|
+
const profiles = page.locator?.("[data-testid=\"accounts-profile-button\"]");
|
|
425
|
+
const profileCount = await profiles?.count?.().catch(() => 0) ?? 0;
|
|
426
|
+
let settings;
|
|
427
|
+
for (let index = profileCount - 1; index >= 0; index -= 1) {
|
|
428
|
+
const profile = profiles?.nth?.(index);
|
|
429
|
+
if (profile?.click === undefined)
|
|
430
|
+
continue;
|
|
431
|
+
if (profile.isVisible !== undefined && !await profile.isVisible().catch(() => false))
|
|
432
|
+
continue;
|
|
433
|
+
await profile.click().catch(() => undefined);
|
|
434
|
+
await wait(Math.min(options.settleMs, 500));
|
|
435
|
+
const candidate = page.locator?.("[data-testid=\"settings-menu-item\"]")?.last?.();
|
|
436
|
+
const candidateCount = await candidate?.count?.().catch(() => 0) ?? 0;
|
|
437
|
+
if (candidateCount === 1 && candidate?.click !== undefined) {
|
|
438
|
+
settings = candidate;
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
353
441
|
}
|
|
442
|
+
if (settings?.click === undefined)
|
|
443
|
+
throw new Error("Settings menu item was not available from any visible profile control.");
|
|
354
444
|
await settings.click();
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
445
|
+
const deadline = Date.now() + 5_000;
|
|
446
|
+
for (;;) {
|
|
447
|
+
if (await isSettingsOpen(page))
|
|
448
|
+
return;
|
|
449
|
+
if (Date.now() >= deadline)
|
|
450
|
+
throw new Error("Settings modal did not open.");
|
|
451
|
+
await wait(Math.min(options.settleMs, 500));
|
|
358
452
|
}
|
|
359
453
|
}
|
|
360
454
|
async function isSettingsOpen(page) {
|
|
@@ -363,11 +457,11 @@ async function isSettingsOpen(page) {
|
|
|
363
457
|
|| document.querySelector("[data-testid='close-button']") !== null
|
|
364
458
|
|| document.querySelectorAll("button[role='combobox']").length >= 3)) ?? false;
|
|
365
459
|
}
|
|
366
|
-
async function openLanguageCombobox(page) {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
460
|
+
async function openLanguageCombobox(page, knownLanguageNames) {
|
|
461
|
+
const match = await findLanguageCombobox(page, knownLanguageNames);
|
|
462
|
+
const combo = page.locator?.("[role='dialog'] button[role='combobox']")?.nth?.(match.index);
|
|
463
|
+
if (combo?.click === undefined)
|
|
464
|
+
throw new Error("Language combobox was not actionable.");
|
|
371
465
|
await combo.click();
|
|
372
466
|
await wait(500);
|
|
373
467
|
}
|
|
@@ -378,10 +472,22 @@ async function clickOptionExact(page, label) {
|
|
|
378
472
|
}
|
|
379
473
|
await option.click();
|
|
380
474
|
}
|
|
381
|
-
async function readSelectedLanguage(page) {
|
|
475
|
+
async function readSelectedLanguage(page, knownLanguageNames) {
|
|
382
476
|
await openSettings(page, { settleMs: DEFAULT_SETTLE_MS });
|
|
383
|
-
|
|
384
|
-
|
|
477
|
+
return (await findLanguageCombobox(page, knownLanguageNames)).selectedLabel;
|
|
478
|
+
}
|
|
479
|
+
async function findLanguageCombobox(page, knownLanguageNames) {
|
|
480
|
+
const values = await page.evaluate?.(() => Array.from(document.querySelectorAll("[role='dialog'] button[role='combobox']"))
|
|
481
|
+
.map((button, index) => ({
|
|
482
|
+
index,
|
|
483
|
+
label: (button.textContent ?? "").replace(/\s+/g, " ").trim()
|
|
484
|
+
}))) ?? [];
|
|
485
|
+
const known = new Set(knownLanguageNames.map(normalized));
|
|
486
|
+
const matches = values.filter(value => known.has(normalized(value.label)));
|
|
487
|
+
if (matches.length !== 1) {
|
|
488
|
+
throw new Error(`Expected one Settings language combobox by selected native-language value; observed ${matches.length}.`);
|
|
489
|
+
}
|
|
490
|
+
return { index: matches[0].index, selectedLabel: matches[0].label };
|
|
385
491
|
}
|
|
386
492
|
async function closeSettingsIfOpen(page) {
|
|
387
493
|
if (!await isSettingsOpen(page))
|
|
@@ -436,81 +542,431 @@ async function closeFloatingMenus(page) {
|
|
|
436
542
|
}
|
|
437
543
|
async function captureIntelligencePicker(page, options) {
|
|
438
544
|
await closeFloatingMenus(page);
|
|
439
|
-
await
|
|
440
|
-
await
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
if (
|
|
444
|
-
|
|
445
|
-
await wait(options.settleMs);
|
|
446
|
-
modelVersionLabels = (await readPickerState(page)).modelVersionLabels;
|
|
545
|
+
const configuration = await captureConfigurationMenu(page, "chat", options.settleMs, options.openVersionSubmenu);
|
|
546
|
+
const proof = await renderedProof(page);
|
|
547
|
+
const model = configuration.rows.find(row => row.axis === "model");
|
|
548
|
+
const effort = configuration.rows.find(row => row.axis === "effort");
|
|
549
|
+
if (model === undefined || effort === undefined || effort.options.length === 0) {
|
|
550
|
+
throw new Error("Chat configuration did not expose ordered Model and Effort options.");
|
|
447
551
|
}
|
|
448
|
-
|
|
552
|
+
const selectedIntelligenceLabel = effort.options.find(option => option.checked)?.label;
|
|
553
|
+
return {
|
|
554
|
+
htmlLang: proof.htmlLang ?? "",
|
|
555
|
+
url: proof.url ?? "",
|
|
556
|
+
menuHeading: configuration.power.axisLabel,
|
|
557
|
+
intelligenceLabels: effort.options.map(option => option.label),
|
|
558
|
+
...(selectedIntelligenceLabel === undefined ? {} : { selectedIntelligenceLabel }),
|
|
559
|
+
versionFamilyLabels: [],
|
|
560
|
+
modelVersionLabels: model.options.map(option => option.label),
|
|
561
|
+
configuration
|
|
562
|
+
};
|
|
449
563
|
}
|
|
450
|
-
async function
|
|
451
|
-
const
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
await
|
|
462
|
-
|
|
463
|
-
|
|
564
|
+
async function captureLocaleSurface(page, options, chatConfiguration) {
|
|
565
|
+
const warnings = [];
|
|
566
|
+
let chatLabel;
|
|
567
|
+
let workLabel;
|
|
568
|
+
let restoredChat = false;
|
|
569
|
+
let result;
|
|
570
|
+
try {
|
|
571
|
+
await waitForOrderedSurfaceOptions(page, options.switchTimeoutMs);
|
|
572
|
+
const mapped = assignChatSelectedSurfaceOptions(await readVisibleSurfaceOptions(page));
|
|
573
|
+
chatLabel = mapped.chatLabel;
|
|
574
|
+
workLabel = mapped.workLabel;
|
|
575
|
+
const chatComposerLabels = await readVisibleComposerLabels(page);
|
|
576
|
+
await clickSurfaceOption(page, workLabel);
|
|
577
|
+
await waitForSurfaceSelection(page, workLabel, options.switchTimeoutMs);
|
|
578
|
+
await wait(options.settleMs);
|
|
579
|
+
const workComposerLabels = await readVisibleComposerLabels(page);
|
|
580
|
+
const workConfiguration = await captureConfigurationMenu(page, "work", options.settleMs, true);
|
|
581
|
+
result = {
|
|
582
|
+
schemaVersion: "chatgpt.browser_control.locale_surface_capture.v1",
|
|
583
|
+
status: "ok",
|
|
584
|
+
chat: {
|
|
585
|
+
optionLabel: chatLabel,
|
|
586
|
+
composerLabels: chatComposerLabels,
|
|
587
|
+
power: chatConfiguration.power,
|
|
588
|
+
advanced: chatConfiguration.advanced,
|
|
589
|
+
configurationRows: chatConfiguration.rows
|
|
590
|
+
},
|
|
591
|
+
work: {
|
|
592
|
+
optionLabel: workLabel,
|
|
593
|
+
composerLabels: workComposerLabels,
|
|
594
|
+
power: workConfiguration.power,
|
|
595
|
+
advanced: workConfiguration.advanced,
|
|
596
|
+
configurationRows: workConfiguration.rows
|
|
597
|
+
},
|
|
598
|
+
warnings
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
result = {
|
|
603
|
+
schemaVersion: "chatgpt.browser_control.locale_surface_capture.v1",
|
|
604
|
+
status: "blocked",
|
|
605
|
+
warnings,
|
|
606
|
+
blocker: {
|
|
607
|
+
kind: "selector_drift",
|
|
608
|
+
code: "locale_surface_capture_failed",
|
|
609
|
+
message: error instanceof Error ? error.message : String(error)
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
finally {
|
|
614
|
+
await closeFloatingMenus(page).catch(() => undefined);
|
|
615
|
+
if (chatLabel !== undefined) {
|
|
616
|
+
try {
|
|
617
|
+
await clickSurfaceOption(page, chatLabel);
|
|
618
|
+
await waitForSurfaceSelection(page, chatLabel, options.switchTimeoutMs);
|
|
619
|
+
restoredChat = true;
|
|
620
|
+
}
|
|
621
|
+
catch (error) {
|
|
622
|
+
warnings.push(`Unable to restore Chat surface: ${error instanceof Error ? error.message : String(error)}`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
464
625
|
}
|
|
465
|
-
|
|
466
|
-
|
|
626
|
+
if (result.status === "ok" && !restoredChat) {
|
|
627
|
+
result = {
|
|
628
|
+
...result,
|
|
629
|
+
status: "blocked",
|
|
630
|
+
blocker: {
|
|
631
|
+
kind: "selector_drift",
|
|
632
|
+
code: "locale_surface_restore_failed",
|
|
633
|
+
message: "The localized Chat/Work surface was captured, but Chat could not be restored."
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
return { ...result, restoredChat };
|
|
638
|
+
}
|
|
639
|
+
async function ensureChatSurfaceSelected(page, options) {
|
|
640
|
+
const mapped = await waitForOrderedSurfaceOptions(page, options.switchTimeoutMs);
|
|
641
|
+
if (mapped.selected === "chat")
|
|
467
642
|
return;
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
643
|
+
await clickSurfaceOption(page, mapped.chatLabel);
|
|
644
|
+
await waitForSurfaceSelection(page, mapped.chatLabel, options.switchTimeoutMs);
|
|
645
|
+
await wait(options.settleMs);
|
|
646
|
+
}
|
|
647
|
+
async function waitForOrderedSurfaceOptions(page, timeoutMs) {
|
|
648
|
+
const deadline = Date.now() + timeoutMs;
|
|
649
|
+
let lastError;
|
|
650
|
+
for (;;) {
|
|
651
|
+
try {
|
|
652
|
+
return assignOrderedSurfaceOptions(await readVisibleSurfaceOptions(page));
|
|
653
|
+
}
|
|
654
|
+
catch (error) {
|
|
655
|
+
lastError = error;
|
|
656
|
+
}
|
|
657
|
+
if (Date.now() >= deadline) {
|
|
658
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
659
|
+
}
|
|
660
|
+
await wait(250);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function readVisibleSurfaceOptions(page) {
|
|
664
|
+
return await page.evaluate?.(() => {
|
|
665
|
+
const visible = (element) => {
|
|
666
|
+
const rect = element.getBoundingClientRect?.();
|
|
667
|
+
const style = window.getComputedStyle?.(element);
|
|
668
|
+
return rect !== undefined && rect.width > 0 && rect.height > 0
|
|
669
|
+
&& style?.display !== "none" && style?.visibility !== "hidden";
|
|
670
|
+
};
|
|
671
|
+
const label = (element) => (element.getAttribute("aria-label")
|
|
672
|
+
?? element.innerText
|
|
673
|
+
?? element.textContent
|
|
674
|
+
?? "").replace(/\s+/g, " ").trim();
|
|
675
|
+
return Array.from(document.querySelectorAll("[role='radio'], input[type='radio']"))
|
|
676
|
+
.filter(element => visible(element) && element.closest("[role='dialog']") === null)
|
|
677
|
+
.map(element => ({
|
|
678
|
+
label: label(element),
|
|
679
|
+
checked: element.getAttribute("aria-checked") === "true"
|
|
680
|
+
|| element.getAttribute("data-state") === "on"
|
|
681
|
+
|| (element.tagName === "INPUT" && element.checked)
|
|
682
|
+
}))
|
|
683
|
+
.filter(option => option.label.length > 0)
|
|
684
|
+
.slice(0, 8);
|
|
685
|
+
}) ?? [];
|
|
686
|
+
}
|
|
687
|
+
async function readVisibleComposerLabels(page) {
|
|
688
|
+
return await page.evaluate?.(() => {
|
|
689
|
+
const visible = (element) => {
|
|
690
|
+
const rect = element.getBoundingClientRect?.();
|
|
691
|
+
const style = window.getComputedStyle?.(element);
|
|
692
|
+
return rect !== undefined && rect.width > 0 && rect.height > 0
|
|
693
|
+
&& style?.display !== "none" && style?.visibility !== "hidden";
|
|
694
|
+
};
|
|
695
|
+
const roots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
|
|
696
|
+
const nodes = roots.flatMap(root => Array.from(root.querySelectorAll("textarea, [contenteditable='true'], [role='textbox'], input")));
|
|
697
|
+
return Array.from(new Set(nodes.filter(visible).map(element => (element.getAttribute("aria-label")
|
|
698
|
+
?? element.getAttribute("placeholder")
|
|
699
|
+
?? element.innerText
|
|
700
|
+
?? element.textContent
|
|
701
|
+
?? "").replace(/\s+/g, " ").trim()).filter(Boolean))).slice(0, 16);
|
|
702
|
+
}) ?? [];
|
|
703
|
+
}
|
|
704
|
+
async function clickSurfaceOption(page, label) {
|
|
705
|
+
const locator = page.getByRole?.("radio", { name: label, exact: true });
|
|
706
|
+
const count = await locator?.count?.().catch(() => 0) ?? 0;
|
|
707
|
+
if (count !== 1 || locator?.click === undefined) {
|
|
708
|
+
throw new Error(`Surface radio ${JSON.stringify(label)} was missing or ambiguous.`);
|
|
709
|
+
}
|
|
710
|
+
await locator.click();
|
|
711
|
+
}
|
|
712
|
+
async function waitForSurfaceSelection(page, label, timeoutMs) {
|
|
713
|
+
const deadline = Date.now() + timeoutMs;
|
|
714
|
+
for (;;) {
|
|
715
|
+
const options = await readVisibleSurfaceOptions(page);
|
|
716
|
+
if (options.some(option => option.label === label && option.checked))
|
|
474
717
|
return;
|
|
718
|
+
if (Date.now() >= deadline)
|
|
719
|
+
throw new Error(`Timed out waiting for surface ${JSON.stringify(label)}.`);
|
|
720
|
+
await wait(250);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
async function captureConfigurationMenu(page, experience, settleMs, captureModelOptions) {
|
|
724
|
+
const expectedRowCount = experience === "chat" ? 2 : 3;
|
|
725
|
+
const opener = await findConfigurationOpener(page, experience);
|
|
726
|
+
const openerLabel = normalized(await opener.innerText?.().catch(() => undefined)
|
|
727
|
+
?? await opener.textContent?.().catch(() => undefined)
|
|
728
|
+
?? "");
|
|
729
|
+
if (openerLabel.length === 0 || opener.click === undefined) {
|
|
730
|
+
throw new Error(`${experienceLabel(experience)} configuration opener did not expose a label and click action.`);
|
|
475
731
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
732
|
+
await opener.click();
|
|
733
|
+
await wait(Math.min(settleMs, 750));
|
|
734
|
+
let root = await waitForRawConfigurationRoot(page, expectedRowCount, 5_000);
|
|
735
|
+
const initialAdvancedExpanded = root.advanced.expanded;
|
|
736
|
+
try {
|
|
737
|
+
if (!root.advanced.expanded) {
|
|
738
|
+
await setAdvancedExpansion(page, true, expectedRowCount);
|
|
739
|
+
root = await readRawConfigurationRoot(page, expectedRowCount);
|
|
740
|
+
}
|
|
741
|
+
const orderedRows = experience === "chat"
|
|
742
|
+
? assignOrderedChatConfigurationRows(root.rows)
|
|
743
|
+
: assignOrderedWorkConfigurationRows(root.rows);
|
|
744
|
+
const rowsWithOptions = [];
|
|
745
|
+
for (const row of orderedRows) {
|
|
746
|
+
const locator = page.getByRole?.("menuitem", { name: row.label, exact: true });
|
|
747
|
+
const rowCount = await locator?.count?.().catch(() => 0) ?? 0;
|
|
748
|
+
if (rowCount !== 1 || locator?.click === undefined) {
|
|
749
|
+
throw new Error(`${experienceLabel(experience)} configuration row ${JSON.stringify(row.label)} was missing or ambiguous.`);
|
|
750
|
+
}
|
|
751
|
+
const shouldCapture = row.axis !== "model" || captureModelOptions;
|
|
752
|
+
let options = [];
|
|
753
|
+
if (shouldCapture) {
|
|
754
|
+
await locator.click();
|
|
755
|
+
await wait(250);
|
|
756
|
+
options = await readVisibleSubmenuOptions(page);
|
|
757
|
+
if (options.length === 0) {
|
|
758
|
+
throw new Error(`${experienceLabel(experience)} ${row.axis} submenu exposed no options.`);
|
|
759
|
+
}
|
|
760
|
+
await page.keyboard?.press?.("Escape").catch(() => undefined);
|
|
761
|
+
await wait(100);
|
|
502
762
|
}
|
|
763
|
+
rowsWithOptions.push({ ...row, options });
|
|
764
|
+
}
|
|
765
|
+
const rows = experience === "chat"
|
|
766
|
+
? assignOrderedChatConfigurationRows(rowsWithOptions)
|
|
767
|
+
: assignOrderedWorkConfigurationRows(rowsWithOptions);
|
|
768
|
+
const effort = rows.find(row => row.axis === "effort");
|
|
769
|
+
const valueLabel = effort?.valueLabel ?? effort?.options.find(option => option.checked)?.label ?? openerLabel;
|
|
770
|
+
return {
|
|
771
|
+
openerLabel,
|
|
772
|
+
power: { ...root.power, valueLabel },
|
|
773
|
+
advanced: { ...root.advanced, initiallyExpanded: initialAdvancedExpanded },
|
|
774
|
+
rows
|
|
503
775
|
};
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
776
|
+
}
|
|
777
|
+
finally {
|
|
778
|
+
await restoreAdvancedExpansion(page, opener, initialAdvancedExpanded, expectedRowCount);
|
|
779
|
+
await closeFloatingMenus(page).catch(() => undefined);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
async function findConfigurationOpener(page, experience) {
|
|
783
|
+
const deadline = Date.now() + 5_000;
|
|
784
|
+
let lastCounts = [];
|
|
785
|
+
for (;;) {
|
|
786
|
+
const candidates = [
|
|
787
|
+
page.locator?.("main form button.__composer-pill")?.last?.(),
|
|
788
|
+
page.locator?.("main button.__composer-pill")?.last?.(),
|
|
789
|
+
page.locator?.("button.__composer-pill")?.last?.()
|
|
790
|
+
];
|
|
791
|
+
lastCounts = [];
|
|
792
|
+
for (const candidate of candidates) {
|
|
793
|
+
const count = await candidate?.count?.().catch(() => 0) ?? 0;
|
|
794
|
+
lastCounts.push(count);
|
|
795
|
+
if (count === 1 && candidate !== undefined)
|
|
796
|
+
return candidate;
|
|
507
797
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
await
|
|
513
|
-
|
|
798
|
+
if (Date.now() >= deadline)
|
|
799
|
+
break;
|
|
800
|
+
await wait(250);
|
|
801
|
+
}
|
|
802
|
+
const diagnostic = await page.evaluate?.(() => ({
|
|
803
|
+
htmlLang: document.documentElement.lang,
|
|
804
|
+
url: location.href.replace(/\/c\/[^/?#]+/i, "/c/sanitized"),
|
|
805
|
+
dialogCount: document.querySelectorAll("[role='dialog']").length,
|
|
806
|
+
composerPillCount: document.querySelectorAll("button.__composer-pill").length,
|
|
807
|
+
mainComposerPillCount: document.querySelectorAll("main button.__composer-pill").length,
|
|
808
|
+
mainTextboxCount: document.querySelectorAll("main textarea, main [contenteditable='true'], main [role='textbox']").length
|
|
809
|
+
})).catch(() => undefined);
|
|
810
|
+
throw new Error(`Expected one ${experienceLabel(experience)} configuration opener; locatorCounts=${lastCounts.join(",")}; state=${JSON.stringify(diagnostic ?? {})}.`);
|
|
811
|
+
}
|
|
812
|
+
async function readRawConfigurationRoot(page, expectedRowCount) {
|
|
813
|
+
const capture = await page.evaluate?.((wantedRowCount) => {
|
|
814
|
+
const visible = (element) => {
|
|
815
|
+
const rect = element.getBoundingClientRect?.();
|
|
816
|
+
const style = window.getComputedStyle?.(element);
|
|
817
|
+
return rect !== undefined && rect.width > 0 && rect.height > 0
|
|
818
|
+
&& style?.display !== "none" && style?.visibility !== "hidden";
|
|
819
|
+
};
|
|
820
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
821
|
+
const menus = Array.from(document.querySelectorAll("[role='menu']")).filter(visible);
|
|
822
|
+
const root = menus.find(menu => {
|
|
823
|
+
const rows = menu.querySelectorAll("[role='menuitem'][data-has-submenu], [role='menuitem'][aria-haspopup='menu']");
|
|
824
|
+
return rows.length === wantedRowCount && menu.querySelector("[role='slider']") !== null;
|
|
825
|
+
});
|
|
826
|
+
if (root === undefined)
|
|
827
|
+
return undefined;
|
|
828
|
+
const slider = root.querySelector("[role='slider']");
|
|
829
|
+
const powerItem = Array.from(root.querySelectorAll("[role='menuitem']"))
|
|
830
|
+
.find(item => !item.hasAttribute("data-has-submenu") && item.getAttribute("aria-label") !== null);
|
|
831
|
+
const advancedItem = Array.from(root.querySelectorAll("[role='menuitem'][aria-expanded]"))
|
|
832
|
+
.find(item => !item.hasAttribute("data-has-submenu") && item.getAttribute("aria-haspopup") !== "menu");
|
|
833
|
+
const minimum = Number(slider?.getAttribute("aria-valuemin"));
|
|
834
|
+
const maximum = Number(slider?.getAttribute("aria-valuemax"));
|
|
835
|
+
const value = Number(slider?.getAttribute("aria-valuenow"));
|
|
836
|
+
if (slider === null || powerItem === undefined || advancedItem === undefined
|
|
837
|
+
|| !Number.isFinite(minimum) || !Number.isFinite(maximum) || !Number.isFinite(value))
|
|
838
|
+
return undefined;
|
|
839
|
+
const rows = Array.from(root.querySelectorAll("[role='menuitem'][data-has-submenu], [role='menuitem'][aria-haspopup='menu']"))
|
|
840
|
+
.filter(visible)
|
|
841
|
+
.map(row => {
|
|
842
|
+
const html = row;
|
|
843
|
+
const axisLabel = normalize(row.querySelector(".truncate")?.textContent ?? "");
|
|
844
|
+
const valueLabel = normalize(row.querySelector("[data-trailing-style='default']")?.textContent ?? "");
|
|
845
|
+
const item = {
|
|
846
|
+
label: normalize(row.getAttribute("aria-label") ?? html.innerText ?? row.textContent ?? ""),
|
|
847
|
+
axisLabel,
|
|
848
|
+
options: []
|
|
849
|
+
};
|
|
850
|
+
if (valueLabel.length > 0)
|
|
851
|
+
item.valueLabel = valueLabel;
|
|
852
|
+
return item;
|
|
853
|
+
});
|
|
854
|
+
const accessibleLabel = normalize(advancedItem.getAttribute("aria-label") ?? "");
|
|
855
|
+
return {
|
|
856
|
+
power: {
|
|
857
|
+
axisLabel: normalize(powerItem.getAttribute("aria-label") ?? powerItem.innerText ?? powerItem.textContent ?? ""),
|
|
858
|
+
minimum,
|
|
859
|
+
maximum,
|
|
860
|
+
value,
|
|
861
|
+
position: value - minimum + 1,
|
|
862
|
+
count: maximum - minimum + 1
|
|
863
|
+
},
|
|
864
|
+
advanced: {
|
|
865
|
+
label: normalize(advancedItem.innerText ?? advancedItem.textContent ?? ""),
|
|
866
|
+
...(accessibleLabel.length === 0 ? {} : { accessibleLabel }),
|
|
867
|
+
expanded: advancedItem.getAttribute("aria-expanded") === "true"
|
|
868
|
+
},
|
|
869
|
+
rows
|
|
870
|
+
};
|
|
871
|
+
}, expectedRowCount);
|
|
872
|
+
if (capture === undefined) {
|
|
873
|
+
throw new Error(`Expected one Power/Advanced menu with ${expectedRowCount} configuration rows.`);
|
|
874
|
+
}
|
|
875
|
+
if (capture.rows.length !== expectedRowCount) {
|
|
876
|
+
throw new Error(`Expected ${expectedRowCount} visible configuration rows; observed ${capture.rows.length}.`);
|
|
877
|
+
}
|
|
878
|
+
if (capture.power.axisLabel.length === 0 || capture.advanced.label.length === 0) {
|
|
879
|
+
throw new Error("Power or Advanced labels were empty.");
|
|
880
|
+
}
|
|
881
|
+
return capture;
|
|
882
|
+
}
|
|
883
|
+
async function waitForRawConfigurationRoot(page, expectedRowCount, timeoutMs) {
|
|
884
|
+
const deadline = Date.now() + timeoutMs;
|
|
885
|
+
let lastError;
|
|
886
|
+
for (;;) {
|
|
887
|
+
try {
|
|
888
|
+
return await readRawConfigurationRoot(page, expectedRowCount);
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
lastError = error;
|
|
892
|
+
}
|
|
893
|
+
if (Date.now() >= deadline) {
|
|
894
|
+
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
895
|
+
}
|
|
896
|
+
await wait(250);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
async function setAdvancedExpansion(page, expanded, expectedRowCount) {
|
|
900
|
+
const root = await readRawConfigurationRoot(page, expectedRowCount);
|
|
901
|
+
if (root.advanced.expanded === expanded)
|
|
902
|
+
return;
|
|
903
|
+
const toggle = page.locator?.("[role='menu'] [role='menuitem'][aria-expanded]:not([data-has-submenu]):not([aria-haspopup='menu'])")?.last?.();
|
|
904
|
+
const count = await toggle?.count?.().catch(() => 0) ?? 0;
|
|
905
|
+
if (count !== 1 || toggle === undefined) {
|
|
906
|
+
throw new Error("Advanced configuration toggle was missing or ambiguous.");
|
|
907
|
+
}
|
|
908
|
+
if (toggle.press !== undefined) {
|
|
909
|
+
await toggle.press("Enter");
|
|
910
|
+
}
|
|
911
|
+
else if (toggle.click !== undefined) {
|
|
912
|
+
await toggle.click();
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
throw new Error("Advanced configuration toggle was not actionable.");
|
|
916
|
+
}
|
|
917
|
+
await wait(250);
|
|
918
|
+
const after = await readRawConfigurationRoot(page, expectedRowCount);
|
|
919
|
+
if (after.advanced.expanded !== expanded) {
|
|
920
|
+
throw new Error(`Advanced configuration toggle did not become ${expanded ? "expanded" : "compact"}.`);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
async function restoreAdvancedExpansion(page, opener, initiallyExpanded, expectedRowCount) {
|
|
924
|
+
await page.keyboard?.press?.("Escape").catch(() => undefined);
|
|
925
|
+
let root = await readRawConfigurationRoot(page, expectedRowCount).catch(() => undefined);
|
|
926
|
+
if (root === undefined) {
|
|
927
|
+
await opener.click?.();
|
|
928
|
+
await wait(200);
|
|
929
|
+
root = await readRawConfigurationRoot(page, expectedRowCount);
|
|
930
|
+
}
|
|
931
|
+
if (root.advanced.expanded !== initiallyExpanded) {
|
|
932
|
+
await setAdvancedExpansion(page, initiallyExpanded, expectedRowCount);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
function experienceLabel(experience) {
|
|
936
|
+
return experience === "chat" ? "Chat" : "Work";
|
|
937
|
+
}
|
|
938
|
+
async function readVisibleSubmenuOptions(page) {
|
|
939
|
+
return await page.evaluate?.(() => {
|
|
940
|
+
const visible = (element) => {
|
|
941
|
+
const rect = element.getBoundingClientRect?.();
|
|
942
|
+
const style = window.getComputedStyle?.(element);
|
|
943
|
+
return rect !== undefined && rect.width > 0 && rect.height > 0
|
|
944
|
+
&& style?.display !== "none" && style?.visibility !== "hidden";
|
|
945
|
+
};
|
|
946
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
947
|
+
const menus = Array.from(document.querySelectorAll("[role='menu']")).filter(visible);
|
|
948
|
+
const submenu = [...menus].reverse().find(menu => menu.querySelector("[role='menuitemradio']") !== null);
|
|
949
|
+
if (submenu === undefined)
|
|
950
|
+
return [];
|
|
951
|
+
return Array.from(submenu.querySelectorAll("[role='menuitemradio']"))
|
|
952
|
+
.filter(visible)
|
|
953
|
+
.map(option => {
|
|
954
|
+
const contentRoot = Array.from(option.children)
|
|
955
|
+
.find(child => !child.hasAttribute("data-trailing-style"));
|
|
956
|
+
const primary = contentRoot?.children.item(0) ?? contentRoot;
|
|
957
|
+
return {
|
|
958
|
+
label: normalize(primary?.textContent
|
|
959
|
+
?? option.querySelector(".truncate")?.textContent
|
|
960
|
+
?? option.getAttribute("aria-label")
|
|
961
|
+
?? option.innerText
|
|
962
|
+
?? option.textContent
|
|
963
|
+
?? ""),
|
|
964
|
+
checked: option.getAttribute("aria-checked") === "true"
|
|
965
|
+
|| option.getAttribute("data-state") === "checked"
|
|
966
|
+
};
|
|
967
|
+
})
|
|
968
|
+
.filter(option => option.label.length > 0);
|
|
969
|
+
}) ?? [];
|
|
514
970
|
}
|
|
515
971
|
async function clickPagePoint(page, point) {
|
|
516
972
|
const pageWithMouse = page;
|
|
@@ -524,83 +980,12 @@ async function clickPagePoint(page, point) {
|
|
|
524
980
|
}
|
|
525
981
|
await body.click({ position: { x: point.x, y: point.y } });
|
|
526
982
|
}
|
|
527
|
-
async function pickerIsOpen(page) {
|
|
528
|
-
await wait(300);
|
|
529
|
-
return page.evaluate?.(() => document.querySelector("[data-testid='composer-intelligence-picker-content']") !== null
|
|
530
|
-
|| document.querySelector("[role='menuitemradio']") !== null) ?? false;
|
|
531
|
-
}
|
|
532
|
-
async function readPickerState(page) {
|
|
533
|
-
return await page.evaluate?.(() => {
|
|
534
|
-
const menu = document.querySelector("[data-testid='composer-intelligence-picker-content']")
|
|
535
|
-
?? Array.from(document.querySelectorAll("[role='menu']")).find(candidate => candidate.querySelector("[role='menuitemradio']"))
|
|
536
|
-
?? document;
|
|
537
|
-
const menuTextLines = (menu.textContent ?? "").split(/\n/).map(line => line.trim()).filter(Boolean);
|
|
538
|
-
const items = Array.from(document.querySelectorAll("[role='menuitemradio'], [role='menuitem']")).map((element) => {
|
|
539
|
-
const label = (element.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
540
|
-
const role = element.getAttribute("role") ?? undefined;
|
|
541
|
-
return {
|
|
542
|
-
label,
|
|
543
|
-
role,
|
|
544
|
-
checked: element.getAttribute("aria-checked") === "true",
|
|
545
|
-
expanded: element.getAttribute("aria-expanded") === "true",
|
|
546
|
-
hasPopup: element.getAttribute("aria-haspopup") ?? undefined,
|
|
547
|
-
testId: element.getAttribute("data-testid") ?? undefined,
|
|
548
|
-
ariaLabel: element.getAttribute("aria-label") ?? undefined,
|
|
549
|
-
};
|
|
550
|
-
}).filter(item => item.label.length > 0 || item.ariaLabel !== undefined || item.testId !== undefined);
|
|
551
|
-
const radioLabels = items
|
|
552
|
-
.filter(item => item.role === "menuitemradio" && item.label.length > 0)
|
|
553
|
-
.map(item => item.label);
|
|
554
|
-
const versionLabelPattern = /^(?:o\d+|\d+(?:\.\d+)?)$/i;
|
|
555
|
-
const modelVersionLabels = radioLabels.filter(label => versionLabelPattern.test(label));
|
|
556
|
-
const intelligenceLabels = radioLabels.filter(label => !versionLabelPattern.test(label));
|
|
557
|
-
const selectedIntelligenceLabel = items.find(item => item.role === "menuitemradio"
|
|
558
|
-
&& item.checked
|
|
559
|
-
&& item.label.length > 0
|
|
560
|
-
&& !versionLabelPattern.test(item.label))?.label;
|
|
561
|
-
const versionFamilyLabels = items
|
|
562
|
-
.filter(item => item.role === "menuitem" && /^GPT[\s-]/i.test(item.label))
|
|
563
|
-
.map(item => item.label);
|
|
564
|
-
const result = {
|
|
565
|
-
htmlLang: document.documentElement.lang,
|
|
566
|
-
url: location.href,
|
|
567
|
-
intelligenceLabels,
|
|
568
|
-
versionFamilyLabels,
|
|
569
|
-
modelVersionLabels,
|
|
570
|
-
items,
|
|
571
|
-
};
|
|
572
|
-
const heading = menuTextLines.find(line => !radioLabels.includes(line) && !/^GPT[\s-]/i.test(line));
|
|
573
|
-
if (heading !== undefined)
|
|
574
|
-
result.menuHeading = heading;
|
|
575
|
-
if (selectedIntelligenceLabel !== undefined)
|
|
576
|
-
result.selectedIntelligenceLabel = selectedIntelligenceLabel;
|
|
577
|
-
return result;
|
|
578
|
-
}) ?? {
|
|
579
|
-
htmlLang: "",
|
|
580
|
-
url: "",
|
|
581
|
-
intelligenceLabels: [],
|
|
582
|
-
versionFamilyLabels: [],
|
|
583
|
-
modelVersionLabels: []
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
async function openVersionSubmenu(page) {
|
|
587
|
-
const gptCandidates = [
|
|
588
|
-
page.getByRole?.("menuitem", { name: /^GPT[\s-]/i })?.last?.(),
|
|
589
|
-
page.locator?.("[role='menuitem']")?.filter?.({ hasText: /^GPT[\s-]/i })?.last?.(),
|
|
590
|
-
];
|
|
591
|
-
for (const gptMenu of gptCandidates) {
|
|
592
|
-
if (gptMenu?.click === undefined)
|
|
593
|
-
continue;
|
|
594
|
-
await gptMenu.click().catch(() => undefined);
|
|
595
|
-
if ((await readPickerState(page)).modelVersionLabels.length > 0)
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
983
|
async function captureGenerationStateLabels(page, options) {
|
|
600
984
|
const warnings = [];
|
|
601
985
|
const before = await readGenerationUiSnapshot(page).catch(() => ({ controls: [], shortLatestAssistantTexts: [] }));
|
|
602
986
|
let submitted = false;
|
|
603
987
|
let stopped = false;
|
|
988
|
+
let stopFailure;
|
|
604
989
|
let active = before;
|
|
605
990
|
let stopLabels = [];
|
|
606
991
|
try {
|
|
@@ -623,11 +1008,14 @@ async function captureGenerationStateLabels(page, options) {
|
|
|
623
1008
|
warnings.push(`Generation probe failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
624
1009
|
}
|
|
625
1010
|
finally {
|
|
626
|
-
if (submitted || stopLabels.length > 0
|
|
627
|
-
|
|
1011
|
+
if (submitted || stopLabels.length > 0) {
|
|
1012
|
+
try {
|
|
1013
|
+
stopped = await stopGenerationIfVisible(page, stopLabels);
|
|
1014
|
+
}
|
|
1015
|
+
catch (error) {
|
|
1016
|
+
stopFailure = error;
|
|
628
1017
|
warnings.push(`Unable to stop generation after probe: ${error instanceof Error ? error.message : String(error)}`);
|
|
629
|
-
|
|
630
|
-
});
|
|
1018
|
+
}
|
|
631
1019
|
await wait(options.settleMs);
|
|
632
1020
|
}
|
|
633
1021
|
}
|
|
@@ -636,6 +1024,9 @@ async function captureGenerationStateLabels(page, options) {
|
|
|
636
1024
|
if (submitted && !stopped) {
|
|
637
1025
|
warnings.push("Generation probe was submitted but no stop action was confirmed.");
|
|
638
1026
|
}
|
|
1027
|
+
if (stopFailure !== undefined) {
|
|
1028
|
+
throw new Error(`Generation probe cleanup could not be verified: ${stopFailure instanceof Error ? stopFailure.message : String(stopFailure)}`);
|
|
1029
|
+
}
|
|
639
1030
|
return {
|
|
640
1031
|
stopLabels,
|
|
641
1032
|
stoppedLabels,
|
|
@@ -766,6 +1157,8 @@ async function readGenerationUiSnapshot(page) {
|
|
|
766
1157
|
const visible = (element) => {
|
|
767
1158
|
if (typeof element.getBoundingClientRect !== "function")
|
|
768
1159
|
return false;
|
|
1160
|
+
if (element.closest("[hidden], [inert], [aria-hidden='true']") !== null)
|
|
1161
|
+
return false;
|
|
769
1162
|
const rect = element.getBoundingClientRect();
|
|
770
1163
|
const style = window.getComputedStyle(element);
|
|
771
1164
|
return rect.width > 0
|
|
@@ -773,13 +1166,19 @@ async function readGenerationUiSnapshot(page) {
|
|
|
773
1166
|
&& style.display !== "none"
|
|
774
1167
|
&& style.visibility !== "hidden"
|
|
775
1168
|
&& style.opacity !== "0"
|
|
776
|
-
&&
|
|
1169
|
+
&& style.pointerEvents !== "none";
|
|
777
1170
|
};
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
1171
|
+
const textboxes = Array.from(document.querySelectorAll("textarea, [contenteditable='true'], [role='textbox']")).filter(visible);
|
|
1172
|
+
const composers = [...new Set(textboxes
|
|
1173
|
+
.map(textbox => textbox.closest("form")
|
|
1174
|
+
?? textbox.closest("[data-testid*='composer' i]")
|
|
1175
|
+
?? textbox.closest("[aria-label*='composer' i]")
|
|
1176
|
+
?? textbox.closest("[class*='composer' i]"))
|
|
1177
|
+
.filter((value) => value !== null))];
|
|
1178
|
+
const controls = composers.length !== 1 ? [] : Array.from(composers[0].querySelectorAll("button, [role='button']"))
|
|
781
1179
|
.filter(visible)
|
|
782
|
-
.filter(
|
|
1180
|
+
.filter(element => !element.disabled
|
|
1181
|
+
&& element.getAttribute("aria-disabled") !== "true")
|
|
783
1182
|
.map(element => {
|
|
784
1183
|
const html = element;
|
|
785
1184
|
const text = normalize(html.innerText || html.textContent);
|
|
@@ -807,10 +1206,11 @@ async function readGenerationUiSnapshot(page) {
|
|
|
807
1206
|
return { controls, shortLatestAssistantTexts };
|
|
808
1207
|
}).catch(() => ({ controls: [], shortLatestAssistantTexts: [] }));
|
|
809
1208
|
}
|
|
810
|
-
function generationStopLabels(before, active) {
|
|
1209
|
+
export function generationStopLabels(before, active) {
|
|
811
1210
|
const beforeLabels = new Set(before.controls.map(control => normalizedControlKey(control.label)));
|
|
812
1211
|
const candidates = active.controls
|
|
813
|
-
.filter(control =>
|
|
1212
|
+
.filter(control => looksLikeStopControl(control))
|
|
1213
|
+
.filter(control => !beforeLabels.has(normalizedControlKey(control.label)) || hasStableStopTestId(control))
|
|
814
1214
|
.map(control => control.label)
|
|
815
1215
|
.filter(isUsefulGenerationLabel);
|
|
816
1216
|
return dedupeStrings(candidates);
|
|
@@ -822,11 +1222,11 @@ function generationStoppedLabels(before, active, afterStop) {
|
|
|
822
1222
|
.filter(isUsefulStoppedText);
|
|
823
1223
|
return dedupeStrings(candidates);
|
|
824
1224
|
}
|
|
825
|
-
function snapshotLooksActive(snapshot) {
|
|
1225
|
+
export function snapshotLooksActive(snapshot) {
|
|
826
1226
|
return snapshot.controls.some(looksLikeStopControl);
|
|
827
1227
|
}
|
|
828
1228
|
function looksLikeStopControl(control) {
|
|
829
|
-
return /stop|
|
|
1229
|
+
return /stop|abort|interromp|unterbrech|gestoppt|arr[eê]t|detener|parar|interrumpir/i.test([
|
|
830
1230
|
control.label,
|
|
831
1231
|
control.text,
|
|
832
1232
|
control.ariaLabel,
|
|
@@ -834,6 +1234,9 @@ function looksLikeStopControl(control) {
|
|
|
834
1234
|
control.testId
|
|
835
1235
|
].filter(Boolean).join(" "));
|
|
836
1236
|
}
|
|
1237
|
+
function hasStableStopTestId(control) {
|
|
1238
|
+
return /(?:^|[-_])stop(?:[-_]|$)/i.test(control.testId ?? "");
|
|
1239
|
+
}
|
|
837
1240
|
function isUsefulGenerationLabel(label) {
|
|
838
1241
|
const normalized = normalizedControlKey(label);
|
|
839
1242
|
if (normalized.length < 2 || normalized.length > 80)
|
|
@@ -852,54 +1255,75 @@ function isUsefulStoppedText(text) {
|
|
|
852
1255
|
return false;
|
|
853
1256
|
return true;
|
|
854
1257
|
}
|
|
855
|
-
async function stopGenerationIfVisible(page, labels) {
|
|
1258
|
+
export async function stopGenerationIfVisible(page, labels) {
|
|
1259
|
+
if (labels.length === 0)
|
|
1260
|
+
return false;
|
|
1261
|
+
let selected;
|
|
856
1262
|
for (const label of labels) {
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return true;
|
|
1263
|
+
const eligible = await scopedStopCandidates(page, label);
|
|
1264
|
+
if (eligible.length === 1 && eligible[0]?.click !== undefined) {
|
|
1265
|
+
selected = eligible[0];
|
|
1266
|
+
break;
|
|
862
1267
|
}
|
|
863
1268
|
}
|
|
864
|
-
if (
|
|
1269
|
+
if (selected?.click === undefined)
|
|
865
1270
|
return false;
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
};
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
1271
|
+
await selected.click();
|
|
1272
|
+
const expiresAt = Date.now() + 2_000;
|
|
1273
|
+
while (Date.now() < expiresAt) {
|
|
1274
|
+
const remaining = (await Promise.all(labels.map(label => scopedStopCandidates(page, label)))).flat();
|
|
1275
|
+
if (remaining.length === 0)
|
|
1276
|
+
return true;
|
|
1277
|
+
await wait(Math.min(100, Math.max(1, expiresAt - Date.now())));
|
|
1278
|
+
}
|
|
1279
|
+
throw new Error("The locale-capture Stop control was clicked, but generation did not become observably inactive.");
|
|
1280
|
+
}
|
|
1281
|
+
async function scopedStopCandidates(page, label) {
|
|
1282
|
+
const candidates = page.getByRole?.("button", { name: label, exact: true });
|
|
1283
|
+
if (candidates?.count === undefined) {
|
|
1284
|
+
throw new Error(`The locale-capture Stop candidates for ${JSON.stringify(label)} could not be enumerated.`);
|
|
1285
|
+
}
|
|
1286
|
+
const count = await candidates.count();
|
|
1287
|
+
const eligible = [];
|
|
1288
|
+
for (let index = 0; index < count; index += 1) {
|
|
1289
|
+
const candidate = count === 1 ? candidates : candidates?.nth?.(index);
|
|
1290
|
+
if (candidate?.isVisible === undefined || candidate.evaluate === undefined) {
|
|
1291
|
+
throw new Error(`Locale-capture Stop candidate ${index + 1} for ${JSON.stringify(label)} could not be inspected.`);
|
|
1292
|
+
}
|
|
1293
|
+
if (!await candidate.isVisible())
|
|
1294
|
+
continue;
|
|
1295
|
+
const scoped = await candidate.evaluate((element) => {
|
|
1296
|
+
const button = element;
|
|
1297
|
+
if (button.disabled || button.getAttribute("aria-disabled") === "true")
|
|
1298
|
+
return false;
|
|
1299
|
+
const visible = (node) => {
|
|
1300
|
+
if (node.hidden || node.closest("[hidden], [inert], [aria-hidden='true']") !== null)
|
|
1301
|
+
return false;
|
|
1302
|
+
const style = window.getComputedStyle(node);
|
|
1303
|
+
const rect = node.getBoundingClientRect();
|
|
1304
|
+
return style.display !== "none" && style.visibility !== "hidden" && style.opacity !== "0"
|
|
1305
|
+
&& style.pointerEvents !== "none" && (rect.width > 0 || rect.height > 0);
|
|
1306
|
+
};
|
|
1307
|
+
const textboxes = Array.from(document.querySelectorAll("textarea, [contenteditable='true'], [role='textbox']")).filter(visible);
|
|
1308
|
+
const composers = [...new Set(textboxes
|
|
1309
|
+
.map(textbox => textbox.closest("form")
|
|
1310
|
+
?? textbox.closest("[data-testid*='composer' i]")
|
|
1311
|
+
?? textbox.closest("[aria-label*='composer' i]")
|
|
1312
|
+
?? textbox.closest("[class*='composer' i]"))
|
|
1313
|
+
.filter((value) => value !== null))];
|
|
1314
|
+
return composers.length === 1 && composers[0].contains(button);
|
|
893
1315
|
});
|
|
894
|
-
if (
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}, [...labels]).catch(() => false);
|
|
1316
|
+
if (scoped)
|
|
1317
|
+
eligible.push(candidate);
|
|
1318
|
+
}
|
|
1319
|
+
return eligible;
|
|
899
1320
|
}
|
|
900
1321
|
function normalizedControlKey(label) {
|
|
901
1322
|
return label.replace(/\s+/g, " ").trim().toLowerCase();
|
|
902
1323
|
}
|
|
1324
|
+
function normalized(value) {
|
|
1325
|
+
return (value ?? "").replace(/\s+/g, " ").trim();
|
|
1326
|
+
}
|
|
903
1327
|
function dedupeStrings(values) {
|
|
904
1328
|
const seen = new Set();
|
|
905
1329
|
const result = [];
|
|
@@ -957,7 +1381,7 @@ async function appendRecord(path, record) {
|
|
|
957
1381
|
}
|
|
958
1382
|
function printCaptureRecord(record, out) {
|
|
959
1383
|
if (record.status === "ok") {
|
|
960
|
-
console.log(`captured ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} intelligence=${record.intelligenceLabels?.length ?? 0} versions=${record.modelVersionLabels?.length ?? 0} generationStop=${record.generationStopLabels?.length ?? 0} generationStopped=${record.generationStoppedLabels?.length ?? 0} out=${out}`);
|
|
1384
|
+
console.log(`captured ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} intelligence=${record.intelligenceLabels?.length ?? 0} versions=${record.modelVersionLabels?.length ?? 0} surfaces=${record.surfaceCapture?.status ?? "not-requested"} generationStop=${record.generationStopLabels?.length ?? 0} generationStopped=${record.generationStoppedLabels?.length ?? 0} out=${out}`);
|
|
961
1385
|
}
|
|
962
1386
|
else {
|
|
963
1387
|
console.log(`blocked ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} code=${record.blocker?.code ?? "unknown"} out=${out}`);
|
|
@@ -1029,9 +1453,6 @@ function packageRoot() {
|
|
|
1029
1453
|
function wait(ms) {
|
|
1030
1454
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1031
1455
|
}
|
|
1032
|
-
function escapeRegExp(value) {
|
|
1033
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1034
|
-
}
|
|
1035
1456
|
if (typeof process !== "undefined" && process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
1036
1457
|
const exitCode = await main();
|
|
1037
1458
|
process.exitCode = exitCode;
|