codex-chatgpt-control 0.3.0-alpha.1 → 0.5.1-alpha.1
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 +17 -0
- package/README.md +42 -2
- package/contracts/v1/fixtures/backend-capabilities.json +10 -0
- package/contracts/v1/fixtures/command-descriptors.json +265 -8
- package/contracts/v1/fixtures/describe-runner-run.json +5 -2
- package/contracts/v1/fixtures/doctor-scenario-preflight.json +34 -2
- package/contracts/v1/fixtures/help-root.json +1 -1
- package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
- package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
- package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
- package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
- package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
- package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
- package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
- package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
- package/contracts/v1/fixtures/surface-work-basic.json +38 -0
- package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
- package/contracts/v1/manifest.json +32 -1
- package/contracts/v1/parity-suite.json +124 -0
- package/contracts/v1/schemas/backend-request.schema.json +15 -0
- package/contracts/v1/schemas/manifest.schema.json +6 -3
- package/contracts/v1/schemas/surface-profile.schema.json +166 -0
- package/dist/codex-chatgpt-control-backend.mjs +3475 -367
- package/dist/codex-chatgpt-control.bundle.mjs +5249 -2102
- package/dist/src/backend/client.d.ts +26 -1
- package/dist/src/backend/client.js +23 -1
- package/dist/src/backend/protocol.d.ts +1 -1
- package/dist/src/backend/protocol.js +10 -0
- package/dist/src/backend/session.js +20 -0
- package/dist/src/browser/attach.d.ts +1 -0
- package/dist/src/browser/attach.js +3 -3
- package/dist/src/browser/downloads.d.ts +1 -1
- package/dist/src/browser/downloads.js +13 -4
- package/dist/src/browser/page-state.js +5 -2
- package/dist/src/client.d.ts +31 -1
- package/dist/src/client.js +69 -14
- package/dist/src/commands/artifacts.js +1 -7
- package/dist/src/commands/configuration.d.ts +15 -0
- package/dist/src/commands/configuration.js +897 -0
- package/dist/src/commands/doctor.js +38 -1
- package/dist/src/commands/experience.d.ts +13 -0
- package/dist/src/commands/experience.js +421 -0
- package/dist/src/commands/files.js +160 -10
- package/dist/src/commands/messages.d.ts +2 -1
- package/dist/src/commands/messages.js +138 -16
- package/dist/src/commands/modes.js +28 -14
- package/dist/src/commands/registry.js +74 -7
- package/dist/src/commands/response-actions.js +1 -7
- package/dist/src/commands/sequence.js +24 -1
- package/dist/src/commands/session.d.ts +2 -0
- package/dist/src/commands/session.js +50 -1
- package/dist/src/commands/threads.js +1 -7
- package/dist/src/commands/work.d.ts +6 -0
- package/dist/src/commands/work.js +338 -0
- package/dist/src/dom/locale/am.d.ts +15 -1
- package/dist/src/dom/locale/am.js +15 -1
- package/dist/src/dom/locale/ar.d.ts +15 -1
- package/dist/src/dom/locale/ar.js +18 -4
- package/dist/src/dom/locale/bg.d.ts +15 -1
- package/dist/src/dom/locale/bg.js +18 -4
- package/dist/src/dom/locale/bn.d.ts +15 -1
- package/dist/src/dom/locale/bn.js +15 -1
- package/dist/src/dom/locale/bs.d.ts +14 -1
- package/dist/src/dom/locale/bs.js +18 -5
- package/dist/src/dom/locale/ca.d.ts +14 -1
- package/dist/src/dom/locale/ca.js +19 -6
- package/dist/src/dom/locale/cs.d.ts +14 -1
- package/dist/src/dom/locale/cs.js +14 -1
- package/dist/src/dom/locale/da.d.ts +14 -1
- package/dist/src/dom/locale/da.js +15 -2
- package/dist/src/dom/locale/de.d.ts +16 -3
- package/dist/src/dom/locale/de.js +18 -5
- package/dist/src/dom/locale/el.d.ts +15 -1
- package/dist/src/dom/locale/el.js +18 -4
- package/dist/src/dom/locale/en.d.ts +27 -2
- package/dist/src/dom/locale/en.js +28 -2
- package/dist/src/dom/locale/es-419.d.ts +15 -1
- package/dist/src/dom/locale/es-419.js +15 -1
- package/dist/src/dom/locale/es-ES.d.ts +15 -1
- package/dist/src/dom/locale/es-ES.js +15 -1
- package/dist/src/dom/locale/et.d.ts +14 -1
- package/dist/src/dom/locale/et.js +14 -1
- package/dist/src/dom/locale/fa.d.ts +14 -1
- package/dist/src/dom/locale/fa.js +16 -3
- package/dist/src/dom/locale/fi.d.ts +15 -1
- package/dist/src/dom/locale/fi.js +15 -1
- package/dist/src/dom/locale/fr-CA.d.ts +13 -1
- package/dist/src/dom/locale/fr-CA.js +13 -1
- package/dist/src/dom/locale/fr-FR.d.ts +16 -3
- package/dist/src/dom/locale/fr-FR.js +20 -7
- package/dist/src/dom/locale/gu.d.ts +15 -1
- package/dist/src/dom/locale/gu.js +19 -5
- package/dist/src/dom/locale/hi.d.ts +15 -1
- package/dist/src/dom/locale/hi.js +17 -3
- package/dist/src/dom/locale/hr.d.ts +14 -1
- package/dist/src/dom/locale/hr.js +17 -4
- package/dist/src/dom/locale/hu.d.ts +15 -1
- package/dist/src/dom/locale/hu.js +18 -4
- package/dist/src/dom/locale/hy.d.ts +15 -1
- package/dist/src/dom/locale/hy.js +18 -4
- package/dist/src/dom/locale/id.d.ts +14 -1
- package/dist/src/dom/locale/id.js +16 -3
- package/dist/src/dom/locale/index.d.ts +17 -2
- package/dist/src/dom/locale/index.js +65 -0
- package/dist/src/dom/locale/is.d.ts +14 -1
- package/dist/src/dom/locale/is.js +14 -1
- package/dist/src/dom/locale/it.d.ts +13 -1
- package/dist/src/dom/locale/it.js +17 -5
- package/dist/src/dom/locale/ja.d.ts +15 -1
- package/dist/src/dom/locale/ja.js +19 -5
- package/dist/src/dom/locale/ka.d.ts +15 -1
- package/dist/src/dom/locale/ka.js +15 -1
- package/dist/src/dom/locale/kk.d.ts +15 -1
- package/dist/src/dom/locale/kk.js +17 -3
- package/dist/src/dom/locale/kn.d.ts +15 -1
- package/dist/src/dom/locale/kn.js +19 -5
- package/dist/src/dom/locale/ko.d.ts +13 -1
- package/dist/src/dom/locale/ko.js +13 -1
- package/dist/src/dom/locale/lt.d.ts +15 -1
- package/dist/src/dom/locale/lt.js +17 -3
- package/dist/src/dom/locale/lv.d.ts +15 -1
- package/dist/src/dom/locale/lv.js +15 -1
- package/dist/src/dom/locale/mk.d.ts +15 -1
- package/dist/src/dom/locale/mk.js +19 -5
- package/dist/src/dom/locale/ml.d.ts +15 -1
- package/dist/src/dom/locale/ml.js +17 -3
- package/dist/src/dom/locale/mn.d.ts +15 -1
- package/dist/src/dom/locale/mn.js +15 -1
- package/dist/src/dom/locale/mr.d.ts +15 -1
- package/dist/src/dom/locale/mr.js +17 -3
- package/dist/src/dom/locale/ms.d.ts +13 -1
- package/dist/src/dom/locale/ms.js +13 -1
- package/dist/src/dom/locale/my.d.ts +15 -1
- package/dist/src/dom/locale/my.js +17 -3
- package/dist/src/dom/locale/nb.d.ts +14 -1
- package/dist/src/dom/locale/nb.js +14 -1
- package/dist/src/dom/locale/nl.d.ts +14 -1
- package/dist/src/dom/locale/nl.js +16 -3
- package/dist/src/dom/locale/pa.d.ts +15 -1
- package/dist/src/dom/locale/pa.js +19 -5
- package/dist/src/dom/locale/pl.d.ts +14 -1
- package/dist/src/dom/locale/pl.js +16 -3
- package/dist/src/dom/locale/pt-BR.d.ts +15 -1
- package/dist/src/dom/locale/pt-BR.js +17 -3
- package/dist/src/dom/locale/pt-PT.d.ts +15 -1
- package/dist/src/dom/locale/pt-PT.js +19 -5
- package/dist/src/dom/locale/ro.d.ts +13 -1
- package/dist/src/dom/locale/ro.js +16 -4
- package/dist/src/dom/locale/ru.d.ts +17 -1
- package/dist/src/dom/locale/ru.js +18 -2
- package/dist/src/dom/locale/sk.d.ts +14 -1
- package/dist/src/dom/locale/sk.js +16 -3
- package/dist/src/dom/locale/sl.d.ts +14 -1
- package/dist/src/dom/locale/sl.js +17 -4
- package/dist/src/dom/locale/so.d.ts +15 -1
- package/dist/src/dom/locale/so.js +17 -3
- package/dist/src/dom/locale/sq.d.ts +13 -1
- package/dist/src/dom/locale/sq.js +17 -5
- package/dist/src/dom/locale/sr.d.ts +17 -1
- package/dist/src/dom/locale/sr.js +18 -2
- package/dist/src/dom/locale/sv.d.ts +14 -1
- package/dist/src/dom/locale/sv.js +14 -1
- package/dist/src/dom/locale/sw.d.ts +15 -1
- package/dist/src/dom/locale/sw.js +15 -1
- package/dist/src/dom/locale/ta.d.ts +15 -1
- package/dist/src/dom/locale/ta.js +19 -5
- package/dist/src/dom/locale/te.d.ts +15 -1
- package/dist/src/dom/locale/te.js +19 -5
- package/dist/src/dom/locale/th.d.ts +7 -1
- package/dist/src/dom/locale/th.js +7 -1
- package/dist/src/dom/locale/tl.d.ts +11 -5
- package/dist/src/dom/locale/tl.js +11 -5
- package/dist/src/dom/locale/tr.d.ts +14 -1
- package/dist/src/dom/locale/tr.js +16 -3
- package/dist/src/dom/locale/types.d.ts +18 -1
- package/dist/src/dom/locale/uk.d.ts +15 -1
- package/dist/src/dom/locale/uk.js +17 -3
- package/dist/src/dom/locale/ur.d.ts +15 -1
- package/dist/src/dom/locale/ur.js +18 -4
- package/dist/src/dom/locale/vi.d.ts +15 -1
- package/dist/src/dom/locale/vi.js +18 -4
- package/dist/src/dom/locale/zh-HK.d.ts +15 -1
- package/dist/src/dom/locale/zh-HK.js +17 -3
- package/dist/src/dom/locale/zh-Hans.d.ts +15 -1
- package/dist/src/dom/locale/zh-Hans.js +19 -5
- package/dist/src/dom/locale/zh-TW.d.ts +15 -1
- package/dist/src/dom/locale/zh-TW.js +17 -3
- package/dist/src/dom/menus.js +26 -2
- package/dist/src/dom/selectors.js +6 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.js +3 -0
- package/dist/src/platform/local-paths.d.ts +1 -0
- package/dist/src/platform/local-paths.js +7 -3
- package/dist/src/runner/responses.d.ts +3 -1
- package/dist/src/runner/responses.js +16 -1
- package/dist/src/runner/result.js +129 -4
- package/dist/src/runner/stream.d.ts +1 -1
- package/dist/src/runner/stream.js +6 -0
- package/dist/src/runner/types.d.ts +35 -1
- package/dist/src/safety/risk.d.ts +10 -0
- package/dist/src/safety/risk.js +10 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +15 -0
- package/dist/src/scripts/apply-intelligence-locale-captures.js +225 -7
- package/dist/src/scripts/capture-intelligence-locales.d.ts +8 -0
- package/dist/src/scripts/capture-intelligence-locales.js +663 -6
- package/dist/src/scripts/capture-surface-profile.d.ts +25 -0
- package/dist/src/scripts/capture-surface-profile.js +354 -0
- package/dist/src/scripts/live-smoke/scenarios.d.ts +2 -0
- package/dist/src/scripts/live-smoke/scenarios.js +347 -5
- package/dist/src/scripts/live-smoke.js +2 -0
- package/dist/src/scripts/locale-capture/surface-graph.d.ts +26 -0
- package/dist/src/scripts/locale-capture/surface-graph.js +43 -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 +248 -2
- package/package.json +9 -3
- package/references/2026-07-16-chat-work-surfaces.md +100 -0
- package/references/agents-runner.md +13 -2
- package/references/backend-protocol.md +26 -9
- package/references/language-coverage.md +51 -3
- package/references/localization.md +52 -6
- package/references/python-parity.md +12 -2
- package/references/responses-adapter.md +12 -1
- package/references/troubleshooting.md +38 -4
|
@@ -0,0 +1,897 @@
|
|
|
1
|
+
import { enumerateVisibleMenuItems } from "../dom/menus.js";
|
|
2
|
+
import { localeLabels } from "../dom/locale-labels.js";
|
|
3
|
+
import { normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
|
|
4
|
+
import { resultError, resultOk } from "../errors.js";
|
|
5
|
+
import { tabIdFromPage } from "../browser/attach.js";
|
|
6
|
+
import { contextFromPage } from "./context.js";
|
|
7
|
+
import { detectExperience, openExperience } from "./experience.js";
|
|
8
|
+
import { setMode } from "./modes.js";
|
|
9
|
+
import { ensurePage } from "./session.js";
|
|
10
|
+
const WORK_AXES = ["model", "effort", "speed"];
|
|
11
|
+
const CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS = 5_000;
|
|
12
|
+
const CONFIGURATION_CONTROL_POLL_MS = 250;
|
|
13
|
+
const CONFIGURATION_AXIS_ORDER = [
|
|
14
|
+
"model",
|
|
15
|
+
"intelligence",
|
|
16
|
+
"effort",
|
|
17
|
+
"speed",
|
|
18
|
+
"modelVersion",
|
|
19
|
+
];
|
|
20
|
+
export async function inspectConfiguration(env, args = {}) {
|
|
21
|
+
const boot = await ensurePage(env);
|
|
22
|
+
if (!boot.ok) {
|
|
23
|
+
return boot;
|
|
24
|
+
}
|
|
25
|
+
const page = env.page;
|
|
26
|
+
try {
|
|
27
|
+
const detected = await detectExperience(env, args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs });
|
|
28
|
+
if (!detected.ok || detected.data === undefined) {
|
|
29
|
+
return forwardFailure(detected);
|
|
30
|
+
}
|
|
31
|
+
if (args.experience !== undefined && detected.data.experience !== args.experience) {
|
|
32
|
+
return {
|
|
33
|
+
ok: false,
|
|
34
|
+
status: "unsupported",
|
|
35
|
+
warnings: [],
|
|
36
|
+
blocker: {
|
|
37
|
+
kind: "selector_drift",
|
|
38
|
+
code: "experience_mismatch",
|
|
39
|
+
fieldPath: "experience",
|
|
40
|
+
message: `Configuration inspection expected ${args.experience}, but the visible composer is ${detected.data.experience}. Call experience.open first or omit the expected experience.`,
|
|
41
|
+
resumable: true
|
|
42
|
+
},
|
|
43
|
+
context: await contextFromPage(page, {
|
|
44
|
+
experience: detected.data.experience,
|
|
45
|
+
selectorProfile: detected.data.selectorProfile
|
|
46
|
+
})
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const experience = detected.data.experience;
|
|
50
|
+
const rootOpened = experience !== "unknown" && await waitForConfigurationRoot(page, experience, args.timeoutMs);
|
|
51
|
+
if (rootOpened) {
|
|
52
|
+
await page.waitForTimeout?.(150);
|
|
53
|
+
}
|
|
54
|
+
const workAdvancedOpened = experience !== "work"
|
|
55
|
+
|| (rootOpened && await ensureWorkAdvancedPanel(page));
|
|
56
|
+
if (experience === "work" && workAdvancedOpened) {
|
|
57
|
+
await page.waitForTimeout?.(150);
|
|
58
|
+
}
|
|
59
|
+
const panel = await readConfigurationPanel(page);
|
|
60
|
+
const rootItems = rootOpened ? await enumerateVisibleMenuItems(page) : [];
|
|
61
|
+
const data = configurationInspectionFromSurface(experience, detected.data.selectorProfile, detected.data.evidence, panel, rootItems);
|
|
62
|
+
if (args.includeOptions !== false && experience === "work" && panel.axisRows.length > 0) {
|
|
63
|
+
for (const axis of WORK_AXES) {
|
|
64
|
+
if (!data.availableAxes.includes(axis))
|
|
65
|
+
continue;
|
|
66
|
+
const options = await inspectWorkAxisOptions(env, axis);
|
|
67
|
+
if (options.length > 0) {
|
|
68
|
+
data.options[axis] = options;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
await closeConfigurationMenus(page);
|
|
72
|
+
}
|
|
73
|
+
const warnings = [];
|
|
74
|
+
if (!rootOpened) {
|
|
75
|
+
warnings.push("No scoped configuration opener was available; inspection is limited to controls already visible in the composer.");
|
|
76
|
+
}
|
|
77
|
+
if (experience === "work" && rootOpened && !workAdvancedOpened) {
|
|
78
|
+
warnings.push("The Work configuration menu opened, but its Advanced model, effort, and speed controls could not be made visible.");
|
|
79
|
+
}
|
|
80
|
+
if (!data.verified) {
|
|
81
|
+
warnings.push("The visible configuration could not be verified from a recognized Chat or Work selector profile.");
|
|
82
|
+
}
|
|
83
|
+
return resultOk(data, await contextFromPage(page, {
|
|
84
|
+
experience: data.experience,
|
|
85
|
+
selectorProfile: data.selectorProfile
|
|
86
|
+
}), warnings);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function waitForConfigurationRoot(page, experience, timeoutMs) {
|
|
93
|
+
const discoveryMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
|
|
94
|
+
const attempts = Math.max(1, Math.ceil(Math.max(0, discoveryMs) / CONFIGURATION_CONTROL_POLL_MS));
|
|
95
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
96
|
+
if (await openConfigurationRoot(page, experience))
|
|
97
|
+
return true;
|
|
98
|
+
if (attempt + 1 < attempts) {
|
|
99
|
+
await page.waitForTimeout?.(CONFIGURATION_CONTROL_POLL_MS);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
export async function applyConfiguration(env, args) {
|
|
105
|
+
const boot = await ensurePage(env);
|
|
106
|
+
if (!boot.ok) {
|
|
107
|
+
return boot;
|
|
108
|
+
}
|
|
109
|
+
const page = env.page;
|
|
110
|
+
const strict = args.strict ?? true;
|
|
111
|
+
try {
|
|
112
|
+
const desired = normalizeDesiredSelection(args.desired);
|
|
113
|
+
if (selectionEntries(desired).length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
status: "unsupported",
|
|
117
|
+
warnings: [],
|
|
118
|
+
blocker: {
|
|
119
|
+
kind: "selector_drift",
|
|
120
|
+
code: "configuration_empty",
|
|
121
|
+
fieldPath: "desired",
|
|
122
|
+
message: "configuration.apply requires at least one desired model, intelligence, effort, speed, or modelVersion value.",
|
|
123
|
+
resumable: false
|
|
124
|
+
},
|
|
125
|
+
context: await contextFromPage(page)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (args.experience !== undefined) {
|
|
129
|
+
const opened = await openExperience(env, {
|
|
130
|
+
experience: args.experience,
|
|
131
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs })
|
|
132
|
+
});
|
|
133
|
+
if (!opened.ok) {
|
|
134
|
+
return forwardFailure(opened);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const beforeResult = await inspectConfiguration(env, {
|
|
138
|
+
...(args.experience === undefined ? {} : { experience: args.experience }),
|
|
139
|
+
includeOptions: true,
|
|
140
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs })
|
|
141
|
+
});
|
|
142
|
+
if (!beforeResult.ok || beforeResult.data === undefined) {
|
|
143
|
+
return forwardFailure(beforeResult);
|
|
144
|
+
}
|
|
145
|
+
const before = beforeResult.data;
|
|
146
|
+
if (before.experience === "unknown") {
|
|
147
|
+
return configurationFailure(page, before, desired, [], "The visible surface is not recognizable as Chat or Work.", "experience_unknown");
|
|
148
|
+
}
|
|
149
|
+
const selected = [];
|
|
150
|
+
for (const [axis, requested] of selectionEntries(desired)) {
|
|
151
|
+
const active = activeConfigurationValue(before, axis);
|
|
152
|
+
if (active !== undefined && configurationValueMatches(active, requested)) {
|
|
153
|
+
selected.push({ axis, requested, selected: active });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const selection = before.experience === "work"
|
|
157
|
+
? await selectWorkAxis(env, axis, requested)
|
|
158
|
+
: await selectChatAxis(env, axis, requested, args.timeoutMs);
|
|
159
|
+
if (selection === undefined) {
|
|
160
|
+
return configurationFailure(page, before, desired, selected, `Configuration option "${requested}" for ${axis} was not found or was ambiguous on the ${before.experience} surface.`, "configuration_option_not_found", before.options[axis]?.map(option => option.label));
|
|
161
|
+
}
|
|
162
|
+
selected.push({ axis, requested, selected: selection });
|
|
163
|
+
}
|
|
164
|
+
const afterResult = await inspectConfiguration(env, {
|
|
165
|
+
...(args.experience === undefined ? {} : { experience: args.experience }),
|
|
166
|
+
includeOptions: false,
|
|
167
|
+
...(args.timeoutMs === undefined ? {} : { timeoutMs: args.timeoutMs })
|
|
168
|
+
});
|
|
169
|
+
if (!afterResult.ok || afterResult.data === undefined) {
|
|
170
|
+
return forwardFailure(afterResult);
|
|
171
|
+
}
|
|
172
|
+
const after = afterResult.data;
|
|
173
|
+
const verified = configurationMatchesSelection(after, desired);
|
|
174
|
+
const data = { requested: desired, selected, before, after, verified };
|
|
175
|
+
if (!verified && strict) {
|
|
176
|
+
return {
|
|
177
|
+
ok: false,
|
|
178
|
+
status: "blocked",
|
|
179
|
+
data,
|
|
180
|
+
warnings: [],
|
|
181
|
+
blocker: {
|
|
182
|
+
kind: "selector_drift",
|
|
183
|
+
code: "configuration_postcondition_unverified",
|
|
184
|
+
fieldPath: "desired",
|
|
185
|
+
message: `ChatGPT accepted configuration clicks, but the visible ${after.experience} controls do not verify every requested value.`,
|
|
186
|
+
candidates: Object.entries(after.active).map(([axis, label]) => ({ label: `${axis}: ${label}` })),
|
|
187
|
+
resumable: true
|
|
188
|
+
},
|
|
189
|
+
context: await contextFromPage(page, {
|
|
190
|
+
experience: after.experience,
|
|
191
|
+
selectorProfile: after.selectorProfile
|
|
192
|
+
})
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const warnings = verified
|
|
196
|
+
? []
|
|
197
|
+
: ["Configuration clicks completed, but strict verification was disabled and the visible postcondition remains unverified."];
|
|
198
|
+
return resultOk(data, await contextFromPage(page, {
|
|
199
|
+
experience: after.experience,
|
|
200
|
+
selectorProfile: after.selectorProfile
|
|
201
|
+
}), warnings);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function configurationInspectionFromSurface(experience, detectedProfile, evidence, panel, menuItems) {
|
|
208
|
+
const active = {};
|
|
209
|
+
const options = {};
|
|
210
|
+
const availableAxes = [];
|
|
211
|
+
let selectorProfile = detectedProfile;
|
|
212
|
+
if (experience === "work") {
|
|
213
|
+
for (const row of panel.axisRows) {
|
|
214
|
+
if (!availableAxes.includes(row.axis))
|
|
215
|
+
availableAxes.push(row.axis);
|
|
216
|
+
if (row.value !== undefined && row.value.length > 0)
|
|
217
|
+
active[row.axis] = row.value;
|
|
218
|
+
}
|
|
219
|
+
selectorProfile = panel.advancedVisible ? "work_advanced_v1" : "work_basic_v1";
|
|
220
|
+
}
|
|
221
|
+
else if (experience === "chat") {
|
|
222
|
+
const simplified = chatMenuLooksSimplified(menuItems);
|
|
223
|
+
selectorProfile = simplified ? "chat_simplified_v1" : detectedProfile;
|
|
224
|
+
const axis = simplified ? "intelligence" : "effort";
|
|
225
|
+
if (menuItems.length > 0 || panel.openerLabel !== undefined) {
|
|
226
|
+
availableAxes.push(axis);
|
|
227
|
+
}
|
|
228
|
+
if (panel.openerLabel !== undefined) {
|
|
229
|
+
active[axis] = panel.openerLabel;
|
|
230
|
+
}
|
|
231
|
+
const chatOptions = menuItems
|
|
232
|
+
.filter(item => !isConfigurationAxisRow(item.label))
|
|
233
|
+
.map(menuItemToOption);
|
|
234
|
+
if (chatOptions.length > 0) {
|
|
235
|
+
options[axis] = chatOptions;
|
|
236
|
+
}
|
|
237
|
+
const modelRows = menuItems.filter(item => /^gpt[\s-]/i.test(item.label) || item.hasPopup === true);
|
|
238
|
+
if (modelRows.length > 0) {
|
|
239
|
+
availableAxes.push("modelVersion");
|
|
240
|
+
options.modelVersion = modelRows.map(menuItemToOption);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
experience,
|
|
245
|
+
selectorProfile,
|
|
246
|
+
availableAxes,
|
|
247
|
+
active,
|
|
248
|
+
options,
|
|
249
|
+
verified: experience !== "unknown" && (availableAxes.length > 0 || Object.keys(active).length > 0),
|
|
250
|
+
evidence
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
async function inspectWorkAxisOptions(env, axis) {
|
|
254
|
+
const page = env.page;
|
|
255
|
+
const options = (await openWorkAxisOptions(env, axis))
|
|
256
|
+
.map(menuItemToOption);
|
|
257
|
+
await closeConfigurationSubmenu(page);
|
|
258
|
+
return dedupeOptions(options);
|
|
259
|
+
}
|
|
260
|
+
async function selectWorkAxis(env, axis, requested) {
|
|
261
|
+
const page = env.page;
|
|
262
|
+
if (!WORK_AXES.includes(axis)) {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
266
|
+
const candidates = await openWorkAxisOptions(env, axis);
|
|
267
|
+
const match = findConfigurationOption(candidates, requested);
|
|
268
|
+
if (match !== undefined && await clickVisibleMenuItem(page, match)) {
|
|
269
|
+
await page.waitForTimeout?.(150);
|
|
270
|
+
return match.label;
|
|
271
|
+
}
|
|
272
|
+
if (attempt === 0) {
|
|
273
|
+
// The submenu can disappear between enumeration and the visible click
|
|
274
|
+
// when ChatGPT's pointer-grace timer fires. Reopen once and click from a
|
|
275
|
+
// fresh DOM instance instead of treating that transient race as drift.
|
|
276
|
+
await closeConfigurationMenus(page);
|
|
277
|
+
await page.waitForTimeout?.(200);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
async function openWorkAxisOptions(env, axis, allowRootRetry = true) {
|
|
283
|
+
const page = env.page;
|
|
284
|
+
if (!await ensureWorkAdvancedPanel(page))
|
|
285
|
+
return [];
|
|
286
|
+
const row = await findWorkAxisRow(page, axis);
|
|
287
|
+
if (row === undefined)
|
|
288
|
+
return [];
|
|
289
|
+
const visibleOptions = async () => filterWorkAxisOptions(await enumerateVisibleMenuItems(page), axis);
|
|
290
|
+
const alreadyOpen = await visibleOptions();
|
|
291
|
+
if (alreadyOpen.length > 0)
|
|
292
|
+
return alreadyOpen;
|
|
293
|
+
const point = await locatorCenter(row);
|
|
294
|
+
if (point !== undefined && await movePointerWithCdp(env, point)) {
|
|
295
|
+
await page.waitForTimeout?.(180);
|
|
296
|
+
const hoveredOptions = await visibleOptions();
|
|
297
|
+
if (hoveredOptions.length > 0)
|
|
298
|
+
return hoveredOptions;
|
|
299
|
+
// Radix may apply the previous submenu's pointer-grace close after the new
|
|
300
|
+
// axis briefly opens. Once that timer has elapsed, a tiny in-row nudge
|
|
301
|
+
// creates a fresh hover transition without ever leaving the root menu.
|
|
302
|
+
await movePointerWithCdp(env, { x: point.x - 2, y: point.y });
|
|
303
|
+
await movePointerWithCdp(env, point);
|
|
304
|
+
await page.waitForTimeout?.(180);
|
|
305
|
+
const retriedOptions = await visibleOptions();
|
|
306
|
+
if (retriedOptions.length > 0)
|
|
307
|
+
return retriedOptions;
|
|
308
|
+
}
|
|
309
|
+
if (point !== undefined && page.mouse?.move !== undefined) {
|
|
310
|
+
try {
|
|
311
|
+
await page.mouse.move(point.x, point.y);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Fall through to the Computer Use and click fallbacks.
|
|
315
|
+
}
|
|
316
|
+
await page.waitForTimeout?.(180);
|
|
317
|
+
const mouseOptions = await visibleOptions();
|
|
318
|
+
if (mouseOptions.length > 0)
|
|
319
|
+
return mouseOptions;
|
|
320
|
+
}
|
|
321
|
+
if (point !== undefined && typeof page.cua?.move === "function") {
|
|
322
|
+
try {
|
|
323
|
+
await page.cua.move(point);
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// Fall through to the click fallback.
|
|
327
|
+
}
|
|
328
|
+
await page.waitForTimeout?.(180);
|
|
329
|
+
const movedOptions = await visibleOptions();
|
|
330
|
+
if (movedOptions.length > 0)
|
|
331
|
+
return movedOptions;
|
|
332
|
+
}
|
|
333
|
+
if (row.click !== undefined) {
|
|
334
|
+
await row.click().catch(() => undefined);
|
|
335
|
+
await page.waitForTimeout?.(180);
|
|
336
|
+
}
|
|
337
|
+
const clickedOptions = await visibleOptions();
|
|
338
|
+
if (clickedOptions.length > 0 || !allowRootRetry)
|
|
339
|
+
return clickedOptions;
|
|
340
|
+
// A submenu pointer-grace dismissal can also close the root menu. Reopen the
|
|
341
|
+
// complete Advanced panel once and resolve a fresh row locator; stale row
|
|
342
|
+
// handles and same-coordinate pointer moves cannot recover a detached menu.
|
|
343
|
+
await closeConfigurationMenus(page);
|
|
344
|
+
await page.waitForTimeout?.(200);
|
|
345
|
+
return openWorkAxisOptions(env, axis, false);
|
|
346
|
+
}
|
|
347
|
+
async function locatorCenter(locator) {
|
|
348
|
+
if (locator.evaluate === undefined)
|
|
349
|
+
return undefined;
|
|
350
|
+
return locator.evaluate(element => {
|
|
351
|
+
const rect = element.getBoundingClientRect();
|
|
352
|
+
return {
|
|
353
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
354
|
+
y: Math.round(rect.top + rect.height / 2)
|
|
355
|
+
};
|
|
356
|
+
}).catch(() => undefined);
|
|
357
|
+
}
|
|
358
|
+
async function movePointerWithCdp(env, point) {
|
|
359
|
+
const page = env.page;
|
|
360
|
+
const tabId = page === undefined ? undefined : tabIdFromPage(page);
|
|
361
|
+
if (tabId === undefined || env.browser?.tabs?.get === undefined)
|
|
362
|
+
return false;
|
|
363
|
+
try {
|
|
364
|
+
const tab = await env.browser.tabs.get(tabId);
|
|
365
|
+
const capability = await tab.capabilities?.get?.("cdp");
|
|
366
|
+
if (capability?.send === undefined)
|
|
367
|
+
return false;
|
|
368
|
+
// Move directly between axis rows. Moving outside the Radix root menu first
|
|
369
|
+
// schedules its dismissal; the submenu can briefly appear and then vanish
|
|
370
|
+
// before the bounded option inspection runs.
|
|
371
|
+
await capability.send("Input.dispatchMouseEvent", {
|
|
372
|
+
type: "mouseMoved",
|
|
373
|
+
x: Math.round(point.x),
|
|
374
|
+
y: Math.round(point.y),
|
|
375
|
+
button: "none",
|
|
376
|
+
buttons: 0,
|
|
377
|
+
pointerType: "mouse"
|
|
378
|
+
});
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function filterWorkAxisOptions(items, axis) {
|
|
386
|
+
return items.filter(item => {
|
|
387
|
+
if (isConfigurationAxisRow(item.label))
|
|
388
|
+
return false;
|
|
389
|
+
// Keep every choice axis-specific. This excludes parent actions such as
|
|
390
|
+
// "Reset to default" and prevents a still-open model submenu from being
|
|
391
|
+
// mistaken for effort or speed choices if Escape is unavailable.
|
|
392
|
+
return workAxisOptionLabelMatches(axis, item.label);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function workAxisOptionLabelMatches(axis, label) {
|
|
396
|
+
if (axis === "model") {
|
|
397
|
+
return /\b(?:gpt[\s-]?\d|sol|luna|terra)\b/i.test(label);
|
|
398
|
+
}
|
|
399
|
+
const candidates = axis === "effort"
|
|
400
|
+
? [
|
|
401
|
+
...localeLabels.configurationOptions.light,
|
|
402
|
+
...localeLabels.configurationOptions.medium,
|
|
403
|
+
...localeLabels.configurationOptions.high,
|
|
404
|
+
...localeLabels.configurationOptions.extraHigh,
|
|
405
|
+
...localeLabels.configurationOptions.max,
|
|
406
|
+
...localeLabels.configurationOptions.ultra,
|
|
407
|
+
]
|
|
408
|
+
: axis === "speed"
|
|
409
|
+
? [
|
|
410
|
+
...localeLabels.configurationOptions.standard,
|
|
411
|
+
...localeLabels.configurationOptions.fast,
|
|
412
|
+
]
|
|
413
|
+
: [];
|
|
414
|
+
return candidates.some(candidate => visibleLabelMatches(label, candidate));
|
|
415
|
+
}
|
|
416
|
+
async function selectChatAxis(env, axis, requested, timeoutMs) {
|
|
417
|
+
const legacyArgs = axis === "modelVersion"
|
|
418
|
+
? { modelVersion: requested }
|
|
419
|
+
: axis === "intelligence"
|
|
420
|
+
? { intelligence: requested }
|
|
421
|
+
: axis === "effort"
|
|
422
|
+
? { effort: requested }
|
|
423
|
+
: axis === "model"
|
|
424
|
+
? { model: requested }
|
|
425
|
+
: undefined;
|
|
426
|
+
if (legacyArgs === undefined) {
|
|
427
|
+
return undefined;
|
|
428
|
+
}
|
|
429
|
+
const result = await setMode(env, {
|
|
430
|
+
...legacyArgs,
|
|
431
|
+
...(timeoutMs === undefined ? {} : { timeoutMs })
|
|
432
|
+
});
|
|
433
|
+
return result.ok ? result.data?.selected.at(-1) : undefined;
|
|
434
|
+
}
|
|
435
|
+
async function openConfigurationRoot(page, experience) {
|
|
436
|
+
const existing = await readConfigurationPanel(page);
|
|
437
|
+
if (existing.axisRows.length > 0) {
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
const existingItems = await enumerateVisibleMenuItems(page).catch(() => []);
|
|
441
|
+
if (configurationMenuLooksRecognized(existingItems, experience, existing.openerLabel)) {
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
if (existing.openerLabel !== undefined
|
|
445
|
+
&& await clickIfUnique(page.getByRole?.("button", { name: existing.openerLabel, exact: true }))) {
|
|
446
|
+
return true;
|
|
447
|
+
}
|
|
448
|
+
if (typeof page.evaluate === "function") {
|
|
449
|
+
const clicked = await page.evaluate((surface) => {
|
|
450
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
451
|
+
const visible = (element) => {
|
|
452
|
+
const html = element;
|
|
453
|
+
const rect = html.getBoundingClientRect?.();
|
|
454
|
+
if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
|
|
455
|
+
return false;
|
|
456
|
+
let current = element;
|
|
457
|
+
while (current !== null) {
|
|
458
|
+
if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
const style = typeof window !== "undefined"
|
|
462
|
+
? window.getComputedStyle?.(current)
|
|
463
|
+
: undefined;
|
|
464
|
+
if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
current = current.parentElement ?? null;
|
|
468
|
+
}
|
|
469
|
+
return true;
|
|
470
|
+
};
|
|
471
|
+
const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
|
|
472
|
+
const main = document.querySelector("main");
|
|
473
|
+
const roots = Array.from(new Set([
|
|
474
|
+
...composerRoots,
|
|
475
|
+
...(main === null ? [] : [main])
|
|
476
|
+
]));
|
|
477
|
+
const controls = Array.from(new Set(roots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button']")))))
|
|
478
|
+
.filter(visible);
|
|
479
|
+
const matches = controls.filter(control => {
|
|
480
|
+
const html = control;
|
|
481
|
+
const label = normalize(control.getAttribute("aria-label") ?? html.innerText ?? control.textContent ?? "");
|
|
482
|
+
const testId = control.getAttribute("data-testid") ?? "";
|
|
483
|
+
if (/send|voice|microphone|attach|upload|add files|plus/i.test(`${label} ${testId}`))
|
|
484
|
+
return false;
|
|
485
|
+
if (/model-switcher|model-selector|mode-selector/i.test(testId))
|
|
486
|
+
return true;
|
|
487
|
+
return surface === "work"
|
|
488
|
+
? /\b(?:gpt|sol|luna|terra|light|medium|high|max|ultra|standard|fast)\b/i.test(label)
|
|
489
|
+
: /\b(?:instant|medium|high|extra high|pro|thinking|extended|gpt)\b/i.test(label);
|
|
490
|
+
});
|
|
491
|
+
if (matches.length !== 1)
|
|
492
|
+
return false;
|
|
493
|
+
matches[0].click();
|
|
494
|
+
return true;
|
|
495
|
+
}, experience).catch(() => false);
|
|
496
|
+
if (clicked)
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
const labels = experience === "work"
|
|
500
|
+
? [
|
|
501
|
+
...localeLabels.configurationOptions.light,
|
|
502
|
+
...localeLabels.configurationOptions.medium,
|
|
503
|
+
...localeLabels.configurationOptions.high,
|
|
504
|
+
...localeLabels.configurationOptions.standard,
|
|
505
|
+
]
|
|
506
|
+
: [
|
|
507
|
+
...localeLabels.configurationOptions.instant,
|
|
508
|
+
...localeLabels.configurationOptions.medium,
|
|
509
|
+
...localeLabels.configurationOptions.high,
|
|
510
|
+
...localeLabels.configurationOptions.extraHigh,
|
|
511
|
+
...localeLabels.configurationOptions.pro,
|
|
512
|
+
...localeLabels.modeOptions.thinking,
|
|
513
|
+
];
|
|
514
|
+
for (const label of labels) {
|
|
515
|
+
if (await clickIfUnique(page.getByRole?.("button", { name: label, exact: true }))) {
|
|
516
|
+
return true;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return false;
|
|
520
|
+
}
|
|
521
|
+
function configurationMenuLooksRecognized(items, experience, openerLabel) {
|
|
522
|
+
if (items.some(item => /(?:model|mode|effort|speed)-(?:switcher|selector)|model-switcher/i.test(item.testId ?? ""))) {
|
|
523
|
+
return true;
|
|
524
|
+
}
|
|
525
|
+
if (experience === "work" && items.some(item => localeLabels.configurationAxes.advanced.some(label => visibleLabelMatches(item.label, label)))) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
if (openerLabel === undefined || items.length === 0) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
const semanticLabels = experience === "work"
|
|
532
|
+
? [
|
|
533
|
+
...localeLabels.configurationOptions.light,
|
|
534
|
+
...localeLabels.configurationOptions.medium,
|
|
535
|
+
...localeLabels.configurationOptions.high,
|
|
536
|
+
...localeLabels.configurationOptions.max,
|
|
537
|
+
...localeLabels.configurationOptions.ultra,
|
|
538
|
+
...localeLabels.configurationOptions.standard,
|
|
539
|
+
...localeLabels.configurationOptions.fast,
|
|
540
|
+
]
|
|
541
|
+
: [
|
|
542
|
+
...localeLabels.configurationOptions.instant,
|
|
543
|
+
...localeLabels.configurationOptions.medium,
|
|
544
|
+
...localeLabels.configurationOptions.high,
|
|
545
|
+
...localeLabels.configurationOptions.extraHigh,
|
|
546
|
+
...localeLabels.configurationOptions.pro,
|
|
547
|
+
...localeLabels.modeOptions.thinking,
|
|
548
|
+
...localeLabels.modeOptions.extended,
|
|
549
|
+
];
|
|
550
|
+
const matched = new Set(items
|
|
551
|
+
.filter(item => semanticLabels.some(label => visibleLabelMatches(item.label, label)))
|
|
552
|
+
.map(item => normalizeConfigurationId(item.label)));
|
|
553
|
+
return matched.size >= 2;
|
|
554
|
+
}
|
|
555
|
+
async function ensureWorkAdvancedPanel(page) {
|
|
556
|
+
const panel = await readConfigurationPanel(page);
|
|
557
|
+
if (panel.axisRows.length > 0)
|
|
558
|
+
return true;
|
|
559
|
+
if (!await openConfigurationRoot(page, "work"))
|
|
560
|
+
return false;
|
|
561
|
+
const reopenedPanel = await readConfigurationPanel(page);
|
|
562
|
+
if (reopenedPanel.axisRows.length > 0)
|
|
563
|
+
return true;
|
|
564
|
+
const items = await enumerateVisibleMenuItems(page);
|
|
565
|
+
const advanced = items.filter(item => localeLabels.configurationAxes.advanced.some(label => visibleLabelMatches(item.label, label)));
|
|
566
|
+
if (advanced.length !== 1 || !await clickVisibleMenuItem(page, advanced[0])) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
await page.waitForTimeout?.(200);
|
|
570
|
+
return (await readConfigurationPanel(page)).axisRows.length > 0;
|
|
571
|
+
}
|
|
572
|
+
async function readConfigurationPanel(page) {
|
|
573
|
+
if (typeof page.evaluate !== "function") {
|
|
574
|
+
return { axisRows: [], advancedVisible: false };
|
|
575
|
+
}
|
|
576
|
+
return page.evaluate((axisLabels) => {
|
|
577
|
+
const normalize = (value) => value.replace(/\s+/g, " ").trim();
|
|
578
|
+
const normalizedAxes = Object.fromEntries(Object.entries(axisLabels).map(([axis, labels]) => [
|
|
579
|
+
axis,
|
|
580
|
+
labels.map(label => normalize(label).toLocaleLowerCase())
|
|
581
|
+
]));
|
|
582
|
+
const visible = (element) => {
|
|
583
|
+
const html = element;
|
|
584
|
+
const rect = html.getBoundingClientRect?.();
|
|
585
|
+
if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
|
|
586
|
+
return false;
|
|
587
|
+
let current = element;
|
|
588
|
+
while (current !== null) {
|
|
589
|
+
if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
const style = typeof window !== "undefined"
|
|
593
|
+
? window.getComputedStyle?.(current)
|
|
594
|
+
: undefined;
|
|
595
|
+
if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
|
|
596
|
+
return false;
|
|
597
|
+
}
|
|
598
|
+
current = current.parentElement ?? null;
|
|
599
|
+
}
|
|
600
|
+
return true;
|
|
601
|
+
};
|
|
602
|
+
const overlays = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper], [data-radix-menu-content]")).filter(visible);
|
|
603
|
+
const roots = overlays.length > 0 ? overlays : Array.from(document.querySelectorAll("main")).filter(visible);
|
|
604
|
+
const rows = roots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option']"))).filter(visible);
|
|
605
|
+
const axisRows = [];
|
|
606
|
+
for (const row of rows) {
|
|
607
|
+
const html = row;
|
|
608
|
+
const label = normalize(row.getAttribute("aria-label") ?? html.innerText ?? row.textContent ?? "");
|
|
609
|
+
const normalized = label.toLocaleLowerCase();
|
|
610
|
+
for (const axis of ["model", "intelligence", "effort", "speed"]) {
|
|
611
|
+
const candidates = normalizedAxes[axis] ?? [];
|
|
612
|
+
const prefix = candidates.find(candidate => normalized === candidate || normalized.startsWith(`${candidate} `));
|
|
613
|
+
if (prefix === undefined)
|
|
614
|
+
continue;
|
|
615
|
+
const value = normalize(label.slice(prefix.length));
|
|
616
|
+
const item = { axis, label };
|
|
617
|
+
if (value.length > 0)
|
|
618
|
+
item.value = value;
|
|
619
|
+
axisRows.push(item);
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
|
|
624
|
+
const main = document.querySelector("main");
|
|
625
|
+
const openerRoots = Array.from(new Set([
|
|
626
|
+
...composerRoots,
|
|
627
|
+
...(main === null ? [] : [main])
|
|
628
|
+
]));
|
|
629
|
+
const openerCandidates = Array.from(new Set(openerRoots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button']")))))
|
|
630
|
+
.filter(visible)
|
|
631
|
+
.map(control => {
|
|
632
|
+
const html = control;
|
|
633
|
+
return {
|
|
634
|
+
label: normalize(control.getAttribute("aria-label") ?? html.innerText ?? control.textContent ?? ""),
|
|
635
|
+
testId: control.getAttribute("data-testid") ?? ""
|
|
636
|
+
};
|
|
637
|
+
})
|
|
638
|
+
.filter(item => !/send|voice|microphone|attach|upload|add files|plus/i.test(`${item.label} ${item.testId}`))
|
|
639
|
+
.filter(item => /model-switcher|model-selector|mode-selector/i.test(item.testId)
|
|
640
|
+
|| /\b(?:gpt|sol|luna|terra|instant|medium|high|extra high|pro|thinking|extended|light|standard|fast)\b/i.test(item.label));
|
|
641
|
+
const result = {
|
|
642
|
+
axisRows,
|
|
643
|
+
advancedVisible: axisRows.length > 0
|
|
644
|
+
};
|
|
645
|
+
if (openerCandidates.length === 1 && openerCandidates[0]?.label.length) {
|
|
646
|
+
result.openerLabel = openerCandidates[0].label;
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}, localeLabels.configurationAxes).catch(() => ({ axisRows: [], advancedVisible: false }));
|
|
650
|
+
}
|
|
651
|
+
async function findWorkAxisRow(page, axis) {
|
|
652
|
+
const labels = axis === "modelVersion" ? [] : localeLabels.configurationAxes[axis] ?? [];
|
|
653
|
+
for (const label of labels) {
|
|
654
|
+
const pattern = new RegExp(`^${escapeRegExp(label)}(?:\\s|$)`, "i");
|
|
655
|
+
for (const role of ["button", "menuitem"]) {
|
|
656
|
+
const locator = page.getByRole?.(role, { name: pattern });
|
|
657
|
+
if (locator?.count !== undefined && await locator.count().catch(() => 0) === 1) {
|
|
658
|
+
return locator;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return undefined;
|
|
663
|
+
}
|
|
664
|
+
async function clickVisibleMenuItem(page, item) {
|
|
665
|
+
if (item.testId !== undefined && await clickIfUnique(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`))) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
for (const role of ["menuitemradio", "menuitem", "option"]) {
|
|
669
|
+
if (await clickIfUnique(page.getByRole?.(role, { name: item.label, exact: true }))) {
|
|
670
|
+
return true;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return clickIfUnique(page.getByText?.(item.label, { exact: true }));
|
|
674
|
+
}
|
|
675
|
+
function findConfigurationOption(items, requested) {
|
|
676
|
+
const normalizedRequested = normalizeConfigurationId(requested);
|
|
677
|
+
const exact = items.filter(item => normalizeConfigurationId(item.label) === normalizedRequested);
|
|
678
|
+
if (exact.length === 1)
|
|
679
|
+
return exact[0];
|
|
680
|
+
const semanticLabels = configurationSemanticLabels(requested);
|
|
681
|
+
for (const wanted of semanticLabels) {
|
|
682
|
+
const matches = items.filter(item => normalizeForLabelMatch(item.label) === normalizeForLabelMatch(wanted)
|
|
683
|
+
|| visibleLabelMatches(item.label, wanted));
|
|
684
|
+
if (matches.length === 1)
|
|
685
|
+
return matches[0];
|
|
686
|
+
}
|
|
687
|
+
return undefined;
|
|
688
|
+
}
|
|
689
|
+
function configurationSemanticLabels(requested) {
|
|
690
|
+
const normalized = normalizeConfigurationId(requested);
|
|
691
|
+
for (const labels of Object.values(localeLabels.configurationOptions)) {
|
|
692
|
+
if (labels.some(label => normalizeConfigurationId(label) === normalized)) {
|
|
693
|
+
return labels;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const labels of Object.values(localeLabels.modeOptions)) {
|
|
697
|
+
if (labels.some(label => normalizeConfigurationId(label) === normalized)) {
|
|
698
|
+
return labels;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return [requested];
|
|
702
|
+
}
|
|
703
|
+
export function configurationMatchesSelection(inspection, desired) {
|
|
704
|
+
return selectionEntries(desired).every(([axis, requested]) => {
|
|
705
|
+
const active = activeConfigurationValue(inspection, axis);
|
|
706
|
+
return active !== undefined && configurationValueMatches(active, requested);
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
function activeConfigurationValue(inspection, axis) {
|
|
710
|
+
const direct = inspection.active[axis];
|
|
711
|
+
if (direct !== undefined || inspection.experience !== "chat") {
|
|
712
|
+
return direct;
|
|
713
|
+
}
|
|
714
|
+
if (axis === "model" || axis === "intelligence") {
|
|
715
|
+
return inspection.active.intelligence ?? inspection.active.effort;
|
|
716
|
+
}
|
|
717
|
+
if (axis === "effort") {
|
|
718
|
+
return inspection.active.effort ?? inspection.active.intelligence;
|
|
719
|
+
}
|
|
720
|
+
return undefined;
|
|
721
|
+
}
|
|
722
|
+
function configurationValueMatches(actual, requested) {
|
|
723
|
+
const normalizedActual = normalizeConfigurationId(actual);
|
|
724
|
+
const normalizedRequested = normalizeConfigurationId(requested);
|
|
725
|
+
if (normalizedActual === normalizedRequested)
|
|
726
|
+
return true;
|
|
727
|
+
return configurationSemanticLabels(requested)
|
|
728
|
+
.some(label => normalizeConfigurationId(label) === normalizedActual);
|
|
729
|
+
}
|
|
730
|
+
function selectionEntries(selection) {
|
|
731
|
+
const entries = [];
|
|
732
|
+
for (const axis of CONFIGURATION_AXIS_ORDER) {
|
|
733
|
+
const value = selection[axis];
|
|
734
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
735
|
+
entries.push([axis, value.trim()]);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return entries;
|
|
739
|
+
}
|
|
740
|
+
function normalizeDesiredSelection(selection) {
|
|
741
|
+
const normalized = {};
|
|
742
|
+
for (const axis of ["model", "intelligence", "effort", "speed"]) {
|
|
743
|
+
const value = selection[axis]?.trim();
|
|
744
|
+
if (value !== undefined && value.length > 0)
|
|
745
|
+
normalized[axis] = value;
|
|
746
|
+
}
|
|
747
|
+
const modelVersion = (selection.modelVersion ?? selection.version)?.trim();
|
|
748
|
+
if (modelVersion !== undefined && modelVersion.length > 0) {
|
|
749
|
+
normalized.modelVersion = modelVersion;
|
|
750
|
+
}
|
|
751
|
+
return normalized;
|
|
752
|
+
}
|
|
753
|
+
function menuItemToOption(item) {
|
|
754
|
+
const option = {
|
|
755
|
+
id: normalizeConfigurationId(item.label),
|
|
756
|
+
label: item.label,
|
|
757
|
+
selected: item.checked === true
|
|
758
|
+
};
|
|
759
|
+
if (item.hasPopup !== undefined)
|
|
760
|
+
option.hasSubmenu = item.hasPopup;
|
|
761
|
+
return option;
|
|
762
|
+
}
|
|
763
|
+
function dedupeOptions(options) {
|
|
764
|
+
const seen = new Set();
|
|
765
|
+
return options.filter(option => {
|
|
766
|
+
const key = `${option.id}\u0000${option.label}`;
|
|
767
|
+
if (seen.has(key))
|
|
768
|
+
return false;
|
|
769
|
+
seen.add(key);
|
|
770
|
+
return true;
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
function chatMenuLooksSimplified(items) {
|
|
774
|
+
const normalized = items.map(item => normalizeConfigurationId(item.label));
|
|
775
|
+
const simplified = ["instant", "medium", "high", "extra high", "pro"];
|
|
776
|
+
return simplified.filter(label => normalized.includes(label)).length >= 3;
|
|
777
|
+
}
|
|
778
|
+
function isConfigurationAxisRow(label) {
|
|
779
|
+
const normalized = normalizeForLabelMatch(label);
|
|
780
|
+
return Object.values(localeLabels.configurationAxes)
|
|
781
|
+
.flat()
|
|
782
|
+
.some(axis => {
|
|
783
|
+
const prefix = normalizeForLabelMatch(axis);
|
|
784
|
+
return normalized === prefix || normalized.startsWith(`${prefix} `);
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
function normalizeConfigurationId(value) {
|
|
788
|
+
return normalizeForLabelMatch(value)
|
|
789
|
+
.replace(/^gpt[\s-]*/i, "gpt ")
|
|
790
|
+
.replace(/[_-]+/g, " ")
|
|
791
|
+
.replace(/\s+/g, " ")
|
|
792
|
+
.trim();
|
|
793
|
+
}
|
|
794
|
+
async function closeConfigurationMenus(page) {
|
|
795
|
+
if (!await pressConfigurationEscape(page))
|
|
796
|
+
return;
|
|
797
|
+
await page.waitForTimeout?.(50);
|
|
798
|
+
await pressConfigurationEscape(page);
|
|
799
|
+
}
|
|
800
|
+
async function closeConfigurationSubmenu(page) {
|
|
801
|
+
if (!await pressConfigurationEscape(page))
|
|
802
|
+
return;
|
|
803
|
+
// Radix retains a short pointer-grace timer after a submenu closes. If the
|
|
804
|
+
// next axis is hovered too quickly, that stale timer can dismiss the newly
|
|
805
|
+
// opened submenu. Let the prior close settle before moving to another row.
|
|
806
|
+
await page.waitForTimeout?.(200);
|
|
807
|
+
}
|
|
808
|
+
async function pressConfigurationEscape(page) {
|
|
809
|
+
if (page.keyboard?.press !== undefined) {
|
|
810
|
+
await page.keyboard.press("Escape");
|
|
811
|
+
return true;
|
|
812
|
+
}
|
|
813
|
+
if (page.cua?.keypress !== undefined) {
|
|
814
|
+
try {
|
|
815
|
+
await page.cua.keypress({ keys: ["ESC"] });
|
|
816
|
+
return true;
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
return false;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
async function clickIfUnique(locator) {
|
|
825
|
+
if (locator?.count === undefined || locator.click === undefined)
|
|
826
|
+
return false;
|
|
827
|
+
const count = await locator.count().catch(() => 0);
|
|
828
|
+
if (count === 1) {
|
|
829
|
+
await locator.click();
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
if (count <= 1 || locator.nth === undefined)
|
|
833
|
+
return false;
|
|
834
|
+
const visible = [];
|
|
835
|
+
for (let index = 0; index < count; index += 1) {
|
|
836
|
+
const candidate = locator.nth(index);
|
|
837
|
+
if (candidate.isVisible !== undefined && await candidate.isVisible().catch(() => false)) {
|
|
838
|
+
visible.push(candidate);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (visible.length !== 1 || visible[0]?.click === undefined)
|
|
842
|
+
return false;
|
|
843
|
+
await visible[0].click();
|
|
844
|
+
return true;
|
|
845
|
+
}
|
|
846
|
+
async function configurationFailure(page, before, desired, selected, message, code, candidates = []) {
|
|
847
|
+
const data = {
|
|
848
|
+
requested: desired,
|
|
849
|
+
selected,
|
|
850
|
+
before,
|
|
851
|
+
after: before,
|
|
852
|
+
verified: false
|
|
853
|
+
};
|
|
854
|
+
return {
|
|
855
|
+
ok: false,
|
|
856
|
+
status: "unsupported",
|
|
857
|
+
data,
|
|
858
|
+
warnings: [],
|
|
859
|
+
blocker: {
|
|
860
|
+
kind: "selector_drift",
|
|
861
|
+
code,
|
|
862
|
+
fieldPath: "desired",
|
|
863
|
+
message,
|
|
864
|
+
candidates: candidates.map(label => ({ label })),
|
|
865
|
+
resumable: true
|
|
866
|
+
},
|
|
867
|
+
context: await contextFromPage(page, {
|
|
868
|
+
experience: before.experience,
|
|
869
|
+
selectorProfile: before.selectorProfile
|
|
870
|
+
})
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function escapeRegExp(value) {
|
|
874
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
875
|
+
}
|
|
876
|
+
function escapeAttributeValue(value) {
|
|
877
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
|
|
878
|
+
}
|
|
879
|
+
function forwardFailure(result) {
|
|
880
|
+
const forwarded = {
|
|
881
|
+
ok: false,
|
|
882
|
+
status: result.status,
|
|
883
|
+
warnings: result.warnings,
|
|
884
|
+
context: result.context
|
|
885
|
+
};
|
|
886
|
+
if (result.output_text !== undefined)
|
|
887
|
+
forwarded.output_text = result.output_text;
|
|
888
|
+
if (result.reportPath !== undefined)
|
|
889
|
+
forwarded.reportPath = result.reportPath;
|
|
890
|
+
if (result.error !== undefined)
|
|
891
|
+
forwarded.error = result.error;
|
|
892
|
+
if (result.blocker !== undefined)
|
|
893
|
+
forwarded.blocker = result.blocker;
|
|
894
|
+
if (result.steps !== undefined)
|
|
895
|
+
forwarded.steps = result.steps;
|
|
896
|
+
return forwarded;
|
|
897
|
+
}
|