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.
Files changed (224) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +42 -2
  3. package/contracts/v1/fixtures/backend-capabilities.json +10 -0
  4. package/contracts/v1/fixtures/command-descriptors.json +265 -8
  5. package/contracts/v1/fixtures/describe-runner-run.json +5 -2
  6. package/contracts/v1/fixtures/doctor-scenario-preflight.json +34 -2
  7. package/contracts/v1/fixtures/help-root.json +1 -1
  8. package/contracts/v1/fixtures/output-json-parse-success.json +9 -0
  9. package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
  10. package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
  11. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  12. package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
  13. package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
  14. package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
  15. package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
  16. package/contracts/v1/fixtures/surface-work-basic.json +38 -0
  17. package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
  18. package/contracts/v1/manifest.json +32 -1
  19. package/contracts/v1/parity-suite.json +124 -0
  20. package/contracts/v1/schemas/backend-request.schema.json +15 -0
  21. package/contracts/v1/schemas/manifest.schema.json +6 -3
  22. package/contracts/v1/schemas/surface-profile.schema.json +166 -0
  23. package/dist/codex-chatgpt-control-backend.mjs +3475 -367
  24. package/dist/codex-chatgpt-control.bundle.mjs +5249 -2102
  25. package/dist/src/backend/client.d.ts +26 -1
  26. package/dist/src/backend/client.js +23 -1
  27. package/dist/src/backend/protocol.d.ts +1 -1
  28. package/dist/src/backend/protocol.js +10 -0
  29. package/dist/src/backend/session.js +20 -0
  30. package/dist/src/browser/attach.d.ts +1 -0
  31. package/dist/src/browser/attach.js +3 -3
  32. package/dist/src/browser/downloads.d.ts +1 -1
  33. package/dist/src/browser/downloads.js +13 -4
  34. package/dist/src/browser/page-state.js +5 -2
  35. package/dist/src/client.d.ts +31 -1
  36. package/dist/src/client.js +69 -14
  37. package/dist/src/commands/artifacts.js +1 -7
  38. package/dist/src/commands/configuration.d.ts +15 -0
  39. package/dist/src/commands/configuration.js +897 -0
  40. package/dist/src/commands/doctor.js +38 -1
  41. package/dist/src/commands/experience.d.ts +13 -0
  42. package/dist/src/commands/experience.js +421 -0
  43. package/dist/src/commands/files.js +160 -10
  44. package/dist/src/commands/messages.d.ts +2 -1
  45. package/dist/src/commands/messages.js +138 -16
  46. package/dist/src/commands/modes.js +28 -14
  47. package/dist/src/commands/registry.js +74 -7
  48. package/dist/src/commands/response-actions.js +1 -7
  49. package/dist/src/commands/sequence.js +24 -1
  50. package/dist/src/commands/session.d.ts +2 -0
  51. package/dist/src/commands/session.js +50 -1
  52. package/dist/src/commands/threads.js +1 -7
  53. package/dist/src/commands/work.d.ts +6 -0
  54. package/dist/src/commands/work.js +338 -0
  55. package/dist/src/dom/locale/am.d.ts +15 -1
  56. package/dist/src/dom/locale/am.js +15 -1
  57. package/dist/src/dom/locale/ar.d.ts +15 -1
  58. package/dist/src/dom/locale/ar.js +18 -4
  59. package/dist/src/dom/locale/bg.d.ts +15 -1
  60. package/dist/src/dom/locale/bg.js +18 -4
  61. package/dist/src/dom/locale/bn.d.ts +15 -1
  62. package/dist/src/dom/locale/bn.js +15 -1
  63. package/dist/src/dom/locale/bs.d.ts +14 -1
  64. package/dist/src/dom/locale/bs.js +18 -5
  65. package/dist/src/dom/locale/ca.d.ts +14 -1
  66. package/dist/src/dom/locale/ca.js +19 -6
  67. package/dist/src/dom/locale/cs.d.ts +14 -1
  68. package/dist/src/dom/locale/cs.js +14 -1
  69. package/dist/src/dom/locale/da.d.ts +14 -1
  70. package/dist/src/dom/locale/da.js +15 -2
  71. package/dist/src/dom/locale/de.d.ts +16 -3
  72. package/dist/src/dom/locale/de.js +18 -5
  73. package/dist/src/dom/locale/el.d.ts +15 -1
  74. package/dist/src/dom/locale/el.js +18 -4
  75. package/dist/src/dom/locale/en.d.ts +27 -2
  76. package/dist/src/dom/locale/en.js +28 -2
  77. package/dist/src/dom/locale/es-419.d.ts +15 -1
  78. package/dist/src/dom/locale/es-419.js +15 -1
  79. package/dist/src/dom/locale/es-ES.d.ts +15 -1
  80. package/dist/src/dom/locale/es-ES.js +15 -1
  81. package/dist/src/dom/locale/et.d.ts +14 -1
  82. package/dist/src/dom/locale/et.js +14 -1
  83. package/dist/src/dom/locale/fa.d.ts +14 -1
  84. package/dist/src/dom/locale/fa.js +16 -3
  85. package/dist/src/dom/locale/fi.d.ts +15 -1
  86. package/dist/src/dom/locale/fi.js +15 -1
  87. package/dist/src/dom/locale/fr-CA.d.ts +13 -1
  88. package/dist/src/dom/locale/fr-CA.js +13 -1
  89. package/dist/src/dom/locale/fr-FR.d.ts +16 -3
  90. package/dist/src/dom/locale/fr-FR.js +20 -7
  91. package/dist/src/dom/locale/gu.d.ts +15 -1
  92. package/dist/src/dom/locale/gu.js +19 -5
  93. package/dist/src/dom/locale/hi.d.ts +15 -1
  94. package/dist/src/dom/locale/hi.js +17 -3
  95. package/dist/src/dom/locale/hr.d.ts +14 -1
  96. package/dist/src/dom/locale/hr.js +17 -4
  97. package/dist/src/dom/locale/hu.d.ts +15 -1
  98. package/dist/src/dom/locale/hu.js +18 -4
  99. package/dist/src/dom/locale/hy.d.ts +15 -1
  100. package/dist/src/dom/locale/hy.js +18 -4
  101. package/dist/src/dom/locale/id.d.ts +14 -1
  102. package/dist/src/dom/locale/id.js +16 -3
  103. package/dist/src/dom/locale/index.d.ts +17 -2
  104. package/dist/src/dom/locale/index.js +65 -0
  105. package/dist/src/dom/locale/is.d.ts +14 -1
  106. package/dist/src/dom/locale/is.js +14 -1
  107. package/dist/src/dom/locale/it.d.ts +13 -1
  108. package/dist/src/dom/locale/it.js +17 -5
  109. package/dist/src/dom/locale/ja.d.ts +15 -1
  110. package/dist/src/dom/locale/ja.js +19 -5
  111. package/dist/src/dom/locale/ka.d.ts +15 -1
  112. package/dist/src/dom/locale/ka.js +15 -1
  113. package/dist/src/dom/locale/kk.d.ts +15 -1
  114. package/dist/src/dom/locale/kk.js +17 -3
  115. package/dist/src/dom/locale/kn.d.ts +15 -1
  116. package/dist/src/dom/locale/kn.js +19 -5
  117. package/dist/src/dom/locale/ko.d.ts +13 -1
  118. package/dist/src/dom/locale/ko.js +13 -1
  119. package/dist/src/dom/locale/lt.d.ts +15 -1
  120. package/dist/src/dom/locale/lt.js +17 -3
  121. package/dist/src/dom/locale/lv.d.ts +15 -1
  122. package/dist/src/dom/locale/lv.js +15 -1
  123. package/dist/src/dom/locale/mk.d.ts +15 -1
  124. package/dist/src/dom/locale/mk.js +19 -5
  125. package/dist/src/dom/locale/ml.d.ts +15 -1
  126. package/dist/src/dom/locale/ml.js +17 -3
  127. package/dist/src/dom/locale/mn.d.ts +15 -1
  128. package/dist/src/dom/locale/mn.js +15 -1
  129. package/dist/src/dom/locale/mr.d.ts +15 -1
  130. package/dist/src/dom/locale/mr.js +17 -3
  131. package/dist/src/dom/locale/ms.d.ts +13 -1
  132. package/dist/src/dom/locale/ms.js +13 -1
  133. package/dist/src/dom/locale/my.d.ts +15 -1
  134. package/dist/src/dom/locale/my.js +17 -3
  135. package/dist/src/dom/locale/nb.d.ts +14 -1
  136. package/dist/src/dom/locale/nb.js +14 -1
  137. package/dist/src/dom/locale/nl.d.ts +14 -1
  138. package/dist/src/dom/locale/nl.js +16 -3
  139. package/dist/src/dom/locale/pa.d.ts +15 -1
  140. package/dist/src/dom/locale/pa.js +19 -5
  141. package/dist/src/dom/locale/pl.d.ts +14 -1
  142. package/dist/src/dom/locale/pl.js +16 -3
  143. package/dist/src/dom/locale/pt-BR.d.ts +15 -1
  144. package/dist/src/dom/locale/pt-BR.js +17 -3
  145. package/dist/src/dom/locale/pt-PT.d.ts +15 -1
  146. package/dist/src/dom/locale/pt-PT.js +19 -5
  147. package/dist/src/dom/locale/ro.d.ts +13 -1
  148. package/dist/src/dom/locale/ro.js +16 -4
  149. package/dist/src/dom/locale/ru.d.ts +17 -1
  150. package/dist/src/dom/locale/ru.js +18 -2
  151. package/dist/src/dom/locale/sk.d.ts +14 -1
  152. package/dist/src/dom/locale/sk.js +16 -3
  153. package/dist/src/dom/locale/sl.d.ts +14 -1
  154. package/dist/src/dom/locale/sl.js +17 -4
  155. package/dist/src/dom/locale/so.d.ts +15 -1
  156. package/dist/src/dom/locale/so.js +17 -3
  157. package/dist/src/dom/locale/sq.d.ts +13 -1
  158. package/dist/src/dom/locale/sq.js +17 -5
  159. package/dist/src/dom/locale/sr.d.ts +17 -1
  160. package/dist/src/dom/locale/sr.js +18 -2
  161. package/dist/src/dom/locale/sv.d.ts +14 -1
  162. package/dist/src/dom/locale/sv.js +14 -1
  163. package/dist/src/dom/locale/sw.d.ts +15 -1
  164. package/dist/src/dom/locale/sw.js +15 -1
  165. package/dist/src/dom/locale/ta.d.ts +15 -1
  166. package/dist/src/dom/locale/ta.js +19 -5
  167. package/dist/src/dom/locale/te.d.ts +15 -1
  168. package/dist/src/dom/locale/te.js +19 -5
  169. package/dist/src/dom/locale/th.d.ts +7 -1
  170. package/dist/src/dom/locale/th.js +7 -1
  171. package/dist/src/dom/locale/tl.d.ts +11 -5
  172. package/dist/src/dom/locale/tl.js +11 -5
  173. package/dist/src/dom/locale/tr.d.ts +14 -1
  174. package/dist/src/dom/locale/tr.js +16 -3
  175. package/dist/src/dom/locale/types.d.ts +18 -1
  176. package/dist/src/dom/locale/uk.d.ts +15 -1
  177. package/dist/src/dom/locale/uk.js +17 -3
  178. package/dist/src/dom/locale/ur.d.ts +15 -1
  179. package/dist/src/dom/locale/ur.js +18 -4
  180. package/dist/src/dom/locale/vi.d.ts +15 -1
  181. package/dist/src/dom/locale/vi.js +18 -4
  182. package/dist/src/dom/locale/zh-HK.d.ts +15 -1
  183. package/dist/src/dom/locale/zh-HK.js +17 -3
  184. package/dist/src/dom/locale/zh-Hans.d.ts +15 -1
  185. package/dist/src/dom/locale/zh-Hans.js +19 -5
  186. package/dist/src/dom/locale/zh-TW.d.ts +15 -1
  187. package/dist/src/dom/locale/zh-TW.js +17 -3
  188. package/dist/src/dom/menus.js +26 -2
  189. package/dist/src/dom/selectors.js +6 -1
  190. package/dist/src/index.d.ts +3 -0
  191. package/dist/src/index.js +3 -0
  192. package/dist/src/platform/local-paths.d.ts +1 -0
  193. package/dist/src/platform/local-paths.js +7 -3
  194. package/dist/src/runner/responses.d.ts +3 -1
  195. package/dist/src/runner/responses.js +16 -1
  196. package/dist/src/runner/result.js +129 -4
  197. package/dist/src/runner/stream.d.ts +1 -1
  198. package/dist/src/runner/stream.js +6 -0
  199. package/dist/src/runner/types.d.ts +35 -1
  200. package/dist/src/safety/risk.d.ts +10 -0
  201. package/dist/src/safety/risk.js +10 -0
  202. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +15 -0
  203. package/dist/src/scripts/apply-intelligence-locale-captures.js +225 -7
  204. package/dist/src/scripts/capture-intelligence-locales.d.ts +8 -0
  205. package/dist/src/scripts/capture-intelligence-locales.js +663 -6
  206. package/dist/src/scripts/capture-surface-profile.d.ts +25 -0
  207. package/dist/src/scripts/capture-surface-profile.js +354 -0
  208. package/dist/src/scripts/live-smoke/scenarios.d.ts +2 -0
  209. package/dist/src/scripts/live-smoke/scenarios.js +347 -5
  210. package/dist/src/scripts/live-smoke.js +2 -0
  211. package/dist/src/scripts/locale-capture/surface-graph.d.ts +26 -0
  212. package/dist/src/scripts/locale-capture/surface-graph.js +43 -0
  213. package/dist/src/scripts/release-canary-module.d.ts +20 -0
  214. package/dist/src/scripts/release-canary-module.js +86 -0
  215. package/dist/src/types.d.ts +248 -2
  216. package/package.json +9 -3
  217. package/references/2026-07-16-chat-work-surfaces.md +100 -0
  218. package/references/agents-runner.md +13 -2
  219. package/references/backend-protocol.md +26 -9
  220. package/references/language-coverage.md +51 -3
  221. package/references/localization.md +52 -6
  222. package/references/python-parity.md +12 -2
  223. package/references/responses-adapter.md +12 -1
  224. 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
+ }