codex-chatgpt-control 0.5.0-alpha.1 → 0.5.1-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/contracts/v1/fixtures/backend-capabilities.json +1 -0
  3. package/contracts/v1/fixtures/command-descriptors.json +29 -2
  4. package/contracts/v1/fixtures/doctor-bridge-upload.json +2 -2
  5. package/contracts/v1/fixtures/doctor-scenario-preflight.json +26 -4
  6. package/contracts/v1/fixtures/files-attach-indeterminate.json +30 -0
  7. package/contracts/v1/fixtures/help-root.json +1 -1
  8. package/contracts/v1/fixtures/messages-stop-indeterminate.json +31 -0
  9. package/contracts/v1/fixtures/messages-stop-needs-confirmation.json +20 -0
  10. package/contracts/v1/fixtures/messages-stop-noop.json +19 -0
  11. package/contracts/v1/fixtures/messages-stop-success.json +21 -0
  12. package/contracts/v1/fixtures/primitive-bootstrap-blocker.json +3 -3
  13. package/contracts/v1/fixtures/reports-create-redacted.json +3 -3
  14. package/contracts/v1/fixtures/reports-summarize-redacted.json +1 -1
  15. package/contracts/v1/fixtures/run-browser-bridge-blocker.json +12 -12
  16. package/contracts/v1/fixtures/stream-blocked.ndjson +2 -2
  17. package/contracts/v1/manifest.json +25 -0
  18. package/contracts/v1/parity-suite.json +19 -1
  19. package/dist/codex-chatgpt-control-backend.mjs +3654 -603
  20. package/dist/codex-chatgpt-control.bundle.mjs +3627 -546
  21. package/dist/src/backend/client.d.ts +2 -1
  22. package/dist/src/backend/client.js +1 -0
  23. package/dist/src/backend/protocol.d.ts +1 -1
  24. package/dist/src/backend/protocol.js +1 -0
  25. package/dist/src/backend/session.js +2 -0
  26. package/dist/src/browser/attach.d.ts +1 -0
  27. package/dist/src/browser/attach.js +69 -35
  28. package/dist/src/browser/chatgpt-url.d.ts +8 -0
  29. package/dist/src/browser/chatgpt-url.js +28 -0
  30. package/dist/src/browser/downloads.d.ts +1 -1
  31. package/dist/src/browser/downloads.js +13 -4
  32. package/dist/src/browser/page-state.js +211 -4
  33. package/dist/src/client.d.ts +2 -1
  34. package/dist/src/client.js +2 -1
  35. package/dist/src/commands/configuration.js +330 -73
  36. package/dist/src/commands/context.d.ts +5 -1
  37. package/dist/src/commands/context.js +2 -2
  38. package/dist/src/commands/conversation.js +13 -1
  39. package/dist/src/commands/doctor.js +24 -2
  40. package/dist/src/commands/experience.d.ts +1 -0
  41. package/dist/src/commands/experience.js +156 -23
  42. package/dist/src/commands/files.d.ts +1 -0
  43. package/dist/src/commands/files.js +877 -170
  44. package/dist/src/commands/messages.d.ts +2 -1
  45. package/dist/src/commands/messages.js +310 -2
  46. package/dist/src/commands/modes.js +197 -52
  47. package/dist/src/commands/registry.js +8 -0
  48. package/dist/src/commands/sequence.js +3 -1
  49. package/dist/src/commands/session.d.ts +4 -1
  50. package/dist/src/commands/session.js +34 -3
  51. package/dist/src/dom/generation-state.d.ts +6 -1
  52. package/dist/src/dom/generation-state.js +206 -45
  53. package/dist/src/dom/locale/am.d.ts +23 -1
  54. package/dist/src/dom/locale/am.js +23 -1
  55. package/dist/src/dom/locale/ar.d.ts +21 -1
  56. package/dist/src/dom/locale/ar.js +24 -4
  57. package/dist/src/dom/locale/bg.d.ts +19 -1
  58. package/dist/src/dom/locale/bg.js +22 -4
  59. package/dist/src/dom/locale/bn.d.ts +23 -1
  60. package/dist/src/dom/locale/bn.js +23 -1
  61. package/dist/src/dom/locale/bs.d.ts +17 -1
  62. package/dist/src/dom/locale/bs.js +21 -5
  63. package/dist/src/dom/locale/ca.d.ts +20 -1
  64. package/dist/src/dom/locale/ca.js +25 -6
  65. package/dist/src/dom/locale/cs.d.ts +17 -1
  66. package/dist/src/dom/locale/cs.js +17 -1
  67. package/dist/src/dom/locale/da.d.ts +17 -1
  68. package/dist/src/dom/locale/da.js +18 -2
  69. package/dist/src/dom/locale/de.d.ts +16 -1
  70. package/dist/src/dom/locale/de.js +18 -3
  71. package/dist/src/dom/locale/el.d.ts +18 -1
  72. package/dist/src/dom/locale/el.js +21 -4
  73. package/dist/src/dom/locale/en.d.ts +2 -1
  74. package/dist/src/dom/locale/en.js +2 -1
  75. package/dist/src/dom/locale/es-419.d.ts +18 -1
  76. package/dist/src/dom/locale/es-419.js +18 -1
  77. package/dist/src/dom/locale/es-ES.d.ts +18 -1
  78. package/dist/src/dom/locale/es-ES.js +18 -1
  79. package/dist/src/dom/locale/et.d.ts +16 -1
  80. package/dist/src/dom/locale/et.js +16 -1
  81. package/dist/src/dom/locale/fa.d.ts +21 -1
  82. package/dist/src/dom/locale/fa.js +23 -3
  83. package/dist/src/dom/locale/fi.d.ts +18 -1
  84. package/dist/src/dom/locale/fi.js +18 -1
  85. package/dist/src/dom/locale/fr-CA.d.ts +15 -1
  86. package/dist/src/dom/locale/fr-CA.js +15 -1
  87. package/dist/src/dom/locale/fr-FR.d.ts +18 -1
  88. package/dist/src/dom/locale/fr-FR.js +22 -5
  89. package/dist/src/dom/locale/gu.d.ts +23 -1
  90. package/dist/src/dom/locale/gu.js +27 -5
  91. package/dist/src/dom/locale/hi.d.ts +23 -1
  92. package/dist/src/dom/locale/hi.js +25 -3
  93. package/dist/src/dom/locale/hr.d.ts +16 -1
  94. package/dist/src/dom/locale/hr.js +19 -4
  95. package/dist/src/dom/locale/hu.d.ts +17 -1
  96. package/dist/src/dom/locale/hu.js +20 -4
  97. package/dist/src/dom/locale/hy.d.ts +23 -1
  98. package/dist/src/dom/locale/hy.js +26 -4
  99. package/dist/src/dom/locale/id.d.ts +17 -1
  100. package/dist/src/dom/locale/id.js +19 -3
  101. package/dist/src/dom/locale/index.js +1 -0
  102. package/dist/src/dom/locale/is.d.ts +21 -1
  103. package/dist/src/dom/locale/is.js +21 -1
  104. package/dist/src/dom/locale/it.d.ts +15 -1
  105. package/dist/src/dom/locale/it.js +20 -6
  106. package/dist/src/dom/locale/ja.d.ts +19 -1
  107. package/dist/src/dom/locale/ja.js +23 -5
  108. package/dist/src/dom/locale/ka.d.ts +23 -1
  109. package/dist/src/dom/locale/ka.js +23 -1
  110. package/dist/src/dom/locale/kk.d.ts +23 -1
  111. package/dist/src/dom/locale/kk.js +25 -3
  112. package/dist/src/dom/locale/kn.d.ts +22 -1
  113. package/dist/src/dom/locale/kn.js +26 -5
  114. package/dist/src/dom/locale/ko.d.ts +17 -1
  115. package/dist/src/dom/locale/ko.js +17 -1
  116. package/dist/src/dom/locale/lt.d.ts +18 -1
  117. package/dist/src/dom/locale/lt.js +20 -3
  118. package/dist/src/dom/locale/lv.d.ts +18 -1
  119. package/dist/src/dom/locale/lv.js +18 -1
  120. package/dist/src/dom/locale/mk.d.ts +22 -1
  121. package/dist/src/dom/locale/mk.js +26 -5
  122. package/dist/src/dom/locale/ml.d.ts +23 -1
  123. package/dist/src/dom/locale/ml.js +25 -3
  124. package/dist/src/dom/locale/mn.d.ts +23 -1
  125. package/dist/src/dom/locale/mn.js +23 -1
  126. package/dist/src/dom/locale/mr.d.ts +23 -1
  127. package/dist/src/dom/locale/mr.js +25 -3
  128. package/dist/src/dom/locale/ms.d.ts +20 -1
  129. package/dist/src/dom/locale/ms.js +20 -1
  130. package/dist/src/dom/locale/my.d.ts +22 -1
  131. package/dist/src/dom/locale/my.js +24 -3
  132. package/dist/src/dom/locale/nb.d.ts +17 -1
  133. package/dist/src/dom/locale/nb.js +17 -1
  134. package/dist/src/dom/locale/nl.d.ts +20 -2
  135. package/dist/src/dom/locale/nl.js +22 -4
  136. package/dist/src/dom/locale/pa.d.ts +23 -1
  137. package/dist/src/dom/locale/pa.js +27 -5
  138. package/dist/src/dom/locale/pl.d.ts +17 -1
  139. package/dist/src/dom/locale/pl.js +19 -3
  140. package/dist/src/dom/locale/pt-BR.d.ts +18 -1
  141. package/dist/src/dom/locale/pt-BR.js +20 -3
  142. package/dist/src/dom/locale/pt-PT.d.ts +18 -1
  143. package/dist/src/dom/locale/pt-PT.js +22 -5
  144. package/dist/src/dom/locale/ro.d.ts +15 -1
  145. package/dist/src/dom/locale/ro.js +18 -4
  146. package/dist/src/dom/locale/ru.d.ts +23 -1
  147. package/dist/src/dom/locale/ru.js +24 -2
  148. package/dist/src/dom/locale/sk.d.ts +17 -1
  149. package/dist/src/dom/locale/sk.js +19 -3
  150. package/dist/src/dom/locale/sl.d.ts +21 -1
  151. package/dist/src/dom/locale/sl.js +24 -4
  152. package/dist/src/dom/locale/so.d.ts +22 -1
  153. package/dist/src/dom/locale/so.js +24 -3
  154. package/dist/src/dom/locale/sq.d.ts +21 -1
  155. package/dist/src/dom/locale/sq.js +25 -5
  156. package/dist/src/dom/locale/sr.d.ts +24 -1
  157. package/dist/src/dom/locale/sr.js +25 -2
  158. package/dist/src/dom/locale/sv.d.ts +16 -1
  159. package/dist/src/dom/locale/sv.js +16 -1
  160. package/dist/src/dom/locale/sw.d.ts +18 -1
  161. package/dist/src/dom/locale/sw.js +18 -1
  162. package/dist/src/dom/locale/ta.d.ts +23 -1
  163. package/dist/src/dom/locale/ta.js +27 -5
  164. package/dist/src/dom/locale/te.d.ts +23 -1
  165. package/dist/src/dom/locale/te.js +27 -5
  166. package/dist/src/dom/locale/th.d.ts +11 -1
  167. package/dist/src/dom/locale/th.js +11 -1
  168. package/dist/src/dom/locale/tl.d.ts +8 -1
  169. package/dist/src/dom/locale/tl.js +8 -1
  170. package/dist/src/dom/locale/tr.d.ts +21 -1
  171. package/dist/src/dom/locale/tr.js +23 -3
  172. package/dist/src/dom/locale/types.d.ts +1 -1
  173. package/dist/src/dom/locale/uk.d.ts +23 -1
  174. package/dist/src/dom/locale/uk.js +25 -3
  175. package/dist/src/dom/locale/ur.d.ts +22 -1
  176. package/dist/src/dom/locale/ur.js +25 -4
  177. package/dist/src/dom/locale/vi.d.ts +22 -1
  178. package/dist/src/dom/locale/vi.js +25 -4
  179. package/dist/src/dom/locale/zh-HK.d.ts +21 -1
  180. package/dist/src/dom/locale/zh-HK.js +23 -3
  181. package/dist/src/dom/locale/zh-Hans.d.ts +23 -1
  182. package/dist/src/dom/locale/zh-Hans.js +27 -5
  183. package/dist/src/dom/locale/zh-TW.d.ts +23 -1
  184. package/dist/src/dom/locale/zh-TW.js +25 -3
  185. package/dist/src/dom/menus.js +26 -2
  186. package/dist/src/dom/selectors.d.ts +1 -0
  187. package/dist/src/dom/selectors.js +9 -1
  188. package/dist/src/dom/wait-snapshot.d.ts +1 -0
  189. package/dist/src/dom/wait-snapshot.js +45 -22
  190. package/dist/src/errors.d.ts +1 -1
  191. package/dist/src/errors.js +2 -2
  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/safety/risk.d.ts +1 -0
  195. package/dist/src/safety/risk.js +1 -0
  196. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +10 -1
  197. package/dist/src/scripts/apply-intelligence-locale-captures.js +219 -7
  198. package/dist/src/scripts/capture-intelligence-locales.d.ts +119 -1
  199. package/dist/src/scripts/capture-intelligence-locales.js +653 -232
  200. package/dist/src/scripts/capture-surface-profile.d.ts +3 -1
  201. package/dist/src/scripts/capture-surface-profile.js +75 -25
  202. package/dist/src/scripts/live-smoke/scenarios.d.ts +3 -0
  203. package/dist/src/scripts/live-smoke/scenarios.js +321 -13
  204. package/dist/src/scripts/live-smoke.js +1 -0
  205. package/dist/src/scripts/locale-capture/surface-graph.d.ts +31 -0
  206. package/dist/src/scripts/locale-capture/surface-graph.js +50 -0
  207. package/dist/src/scripts/release-canary-module.d.ts +20 -0
  208. package/dist/src/scripts/release-canary-module.js +86 -0
  209. package/dist/src/types.d.ts +30 -6
  210. package/package.json +8 -2
  211. package/references/2026-07-16-chat-work-surfaces.md +13 -0
  212. package/references/2026-08-08-model-selection-locale-sweep.md +32 -0
  213. package/references/backend-protocol.md +22 -7
  214. package/references/language-coverage.md +24 -13
  215. package/references/localization.md +36 -14
  216. package/references/python-parity.md +9 -3
  217. package/references/troubleshooting.md +26 -4
@@ -2,11 +2,14 @@ import { enumerateVisibleMenuItems } from "../dom/menus.js";
2
2
  import { localeLabels } from "../dom/locale-labels.js";
3
3
  import { normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
4
4
  import { resultError, resultOk } from "../errors.js";
5
+ import { tabIdFromPage } from "../browser/attach.js";
5
6
  import { contextFromPage } from "./context.js";
6
7
  import { detectExperience, openExperience } from "./experience.js";
7
8
  import { setMode } from "./modes.js";
8
9
  import { ensurePage } from "./session.js";
9
10
  const WORK_AXES = ["model", "effort", "speed"];
11
+ const CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS = 5_000;
12
+ const CONFIGURATION_CONTROL_POLL_MS = 250;
10
13
  const CONFIGURATION_AXIS_ORDER = [
11
14
  "model",
12
15
  "intelligence",
@@ -44,18 +47,27 @@ export async function inspectConfiguration(env, args = {}) {
44
47
  };
45
48
  }
46
49
  const experience = detected.data.experience;
47
- const rootOpened = experience !== "unknown" && await openConfigurationRoot(page, experience);
50
+ const initialPanel = await readConfigurationPanel(page);
51
+ const rootOpened = experience !== "unknown" && await waitForConfigurationRoot(page, experience, args.timeoutMs);
48
52
  if (rootOpened) {
49
53
  await page.waitForTimeout?.(150);
50
54
  }
55
+ const workAdvancedOpened = experience !== "work"
56
+ || (rootOpened && await ensureWorkAdvancedPanel(page));
57
+ if (experience === "work" && workAdvancedOpened) {
58
+ await page.waitForTimeout?.(150);
59
+ }
51
60
  const panel = await readConfigurationPanel(page);
61
+ if (panel.openerLabel === undefined && initialPanel.openerLabel !== undefined) {
62
+ panel.openerLabel = initialPanel.openerLabel;
63
+ }
52
64
  const rootItems = rootOpened ? await enumerateVisibleMenuItems(page) : [];
53
65
  const data = configurationInspectionFromSurface(experience, detected.data.selectorProfile, detected.data.evidence, panel, rootItems);
54
66
  if (args.includeOptions !== false && experience === "work" && panel.axisRows.length > 0) {
55
67
  for (const axis of WORK_AXES) {
56
68
  if (!data.availableAxes.includes(axis))
57
69
  continue;
58
- const options = await inspectWorkAxisOptions(page, axis);
70
+ const options = await inspectWorkAxisOptions(env, axis);
59
71
  if (options.length > 0) {
60
72
  data.options[axis] = options;
61
73
  }
@@ -66,6 +78,9 @@ export async function inspectConfiguration(env, args = {}) {
66
78
  if (!rootOpened) {
67
79
  warnings.push("No scoped configuration opener was available; inspection is limited to controls already visible in the composer.");
68
80
  }
81
+ if (experience === "work" && rootOpened && !workAdvancedOpened) {
82
+ warnings.push("The Work configuration menu opened, but its Advanced model, effort, and speed controls could not be made visible.");
83
+ }
69
84
  if (!data.verified) {
70
85
  warnings.push("The visible configuration could not be verified from a recognized Chat or Work selector profile.");
71
86
  }
@@ -78,6 +93,18 @@ export async function inspectConfiguration(env, args = {}) {
78
93
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
79
94
  }
80
95
  }
96
+ async function waitForConfigurationRoot(page, experience, timeoutMs) {
97
+ const discoveryMs = Math.min(timeoutMs ?? CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS, CONFIGURATION_CONTROL_DISCOVERY_TIMEOUT_MS);
98
+ const attempts = Math.max(1, Math.ceil(Math.max(0, discoveryMs) / CONFIGURATION_CONTROL_POLL_MS));
99
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
100
+ if (await openConfigurationRoot(page, experience))
101
+ return true;
102
+ if (attempt + 1 < attempts) {
103
+ await page.waitForTimeout?.(CONFIGURATION_CONTROL_POLL_MS);
104
+ }
105
+ }
106
+ return false;
107
+ }
81
108
  export async function applyConfiguration(env, args) {
82
109
  const boot = await ensurePage(env);
83
110
  if (!boot.ok) {
@@ -131,7 +158,7 @@ export async function applyConfiguration(env, args) {
131
158
  continue;
132
159
  }
133
160
  const selection = before.experience === "work"
134
- ? await selectWorkAxis(page, axis, requested)
161
+ ? await selectWorkAxis(env, axis, requested)
135
162
  : await selectChatAxis(env, axis, requested, args.timeoutMs);
136
163
  if (selection === undefined) {
137
164
  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));
@@ -227,36 +254,168 @@ export function configurationInspectionFromSurface(experience, detectedProfile,
227
254
  evidence
228
255
  };
229
256
  }
230
- async function inspectWorkAxisOptions(page, axis) {
231
- await ensureWorkConfigurationRoot(page);
232
- if (!await clickWorkAxisRow(page, axis)) {
233
- return [];
234
- }
235
- await page.waitForTimeout?.(120);
236
- const items = await enumerateVisibleMenuItems(page);
237
- const options = items
238
- .filter(item => !isConfigurationAxisRow(item.label))
257
+ async function inspectWorkAxisOptions(env, axis) {
258
+ const page = env.page;
259
+ const options = (await openWorkAxisOptions(env, axis))
239
260
  .map(menuItemToOption);
240
- await page.keyboard?.press?.("Escape");
241
- await page.waitForTimeout?.(80);
261
+ await closeConfigurationSubmenu(page);
242
262
  return dedupeOptions(options);
243
263
  }
244
- async function selectWorkAxis(page, axis, requested) {
264
+ async function selectWorkAxis(env, axis, requested) {
265
+ const page = env.page;
245
266
  if (!WORK_AXES.includes(axis)) {
246
267
  return undefined;
247
268
  }
248
- await ensureWorkConfigurationRoot(page);
249
- if (!await clickWorkAxisRow(page, axis)) {
250
- return undefined;
269
+ for (let attempt = 0; attempt < 2; attempt += 1) {
270
+ const candidates = await openWorkAxisOptions(env, axis);
271
+ const match = findConfigurationOption(candidates, requested);
272
+ if (match !== undefined && await clickVisibleMenuItem(page, match)) {
273
+ await page.waitForTimeout?.(150);
274
+ return match.label;
275
+ }
276
+ if (attempt === 0) {
277
+ // The submenu can disappear between enumeration and the visible click
278
+ // when ChatGPT's pointer-grace timer fires. Reopen once and click from a
279
+ // fresh DOM instance instead of treating that transient race as drift.
280
+ await closeConfigurationMenus(page);
281
+ await page.waitForTimeout?.(200);
282
+ }
251
283
  }
252
- await page.waitForTimeout?.(120);
253
- const candidates = await enumerateVisibleMenuItems(page);
254
- const match = findConfigurationOption(candidates, requested);
255
- if (match === undefined || !await clickVisibleMenuItem(page, match)) {
284
+ return undefined;
285
+ }
286
+ async function openWorkAxisOptions(env, axis, allowRootRetry = true) {
287
+ const page = env.page;
288
+ if (!await ensureWorkAdvancedPanel(page))
289
+ return [];
290
+ const row = await findWorkAxisRow(page, axis);
291
+ if (row === undefined)
292
+ return [];
293
+ const visibleOptions = async () => filterWorkAxisOptions(await enumerateVisibleMenuItems(page), axis);
294
+ const alreadyOpen = await visibleOptions();
295
+ if (alreadyOpen.length > 0)
296
+ return alreadyOpen;
297
+ const point = await locatorCenter(row);
298
+ if (point !== undefined && await movePointerWithCdp(env, point)) {
299
+ await page.waitForTimeout?.(180);
300
+ const hoveredOptions = await visibleOptions();
301
+ if (hoveredOptions.length > 0)
302
+ return hoveredOptions;
303
+ // Radix may apply the previous submenu's pointer-grace close after the new
304
+ // axis briefly opens. Once that timer has elapsed, a tiny in-row nudge
305
+ // creates a fresh hover transition without ever leaving the root menu.
306
+ await movePointerWithCdp(env, { x: point.x - 2, y: point.y });
307
+ await movePointerWithCdp(env, point);
308
+ await page.waitForTimeout?.(180);
309
+ const retriedOptions = await visibleOptions();
310
+ if (retriedOptions.length > 0)
311
+ return retriedOptions;
312
+ }
313
+ if (point !== undefined && page.mouse?.move !== undefined) {
314
+ try {
315
+ await page.mouse.move(point.x, point.y);
316
+ }
317
+ catch {
318
+ // Fall through to the Computer Use and click fallbacks.
319
+ }
320
+ await page.waitForTimeout?.(180);
321
+ const mouseOptions = await visibleOptions();
322
+ if (mouseOptions.length > 0)
323
+ return mouseOptions;
324
+ }
325
+ if (point !== undefined && typeof page.cua?.move === "function") {
326
+ try {
327
+ await page.cua.move(point);
328
+ }
329
+ catch {
330
+ // Fall through to the click fallback.
331
+ }
332
+ await page.waitForTimeout?.(180);
333
+ const movedOptions = await visibleOptions();
334
+ if (movedOptions.length > 0)
335
+ return movedOptions;
336
+ }
337
+ if (row.click !== undefined) {
338
+ await row.click().catch(() => undefined);
339
+ await page.waitForTimeout?.(180);
340
+ }
341
+ const clickedOptions = await visibleOptions();
342
+ if (clickedOptions.length > 0 || !allowRootRetry)
343
+ return clickedOptions;
344
+ // A submenu pointer-grace dismissal can also close the root menu. Reopen the
345
+ // complete Advanced panel once and resolve a fresh row locator; stale row
346
+ // handles and same-coordinate pointer moves cannot recover a detached menu.
347
+ await closeConfigurationMenus(page);
348
+ await page.waitForTimeout?.(200);
349
+ return openWorkAxisOptions(env, axis, false);
350
+ }
351
+ async function locatorCenter(locator) {
352
+ if (locator.evaluate === undefined)
256
353
  return undefined;
354
+ return locator.evaluate(element => {
355
+ const rect = element.getBoundingClientRect();
356
+ return {
357
+ x: Math.round(rect.left + rect.width / 2),
358
+ y: Math.round(rect.top + rect.height / 2)
359
+ };
360
+ }).catch(() => undefined);
361
+ }
362
+ async function movePointerWithCdp(env, point) {
363
+ const page = env.page;
364
+ const tabId = page === undefined ? undefined : tabIdFromPage(page);
365
+ if (tabId === undefined || env.browser?.tabs?.get === undefined)
366
+ return false;
367
+ try {
368
+ const tab = await env.browser.tabs.get(tabId);
369
+ const capability = await tab.capabilities?.get?.("cdp");
370
+ if (capability?.send === undefined)
371
+ return false;
372
+ // Move directly between axis rows. Moving outside the Radix root menu first
373
+ // schedules its dismissal; the submenu can briefly appear and then vanish
374
+ // before the bounded option inspection runs.
375
+ await capability.send("Input.dispatchMouseEvent", {
376
+ type: "mouseMoved",
377
+ x: Math.round(point.x),
378
+ y: Math.round(point.y),
379
+ button: "none",
380
+ buttons: 0,
381
+ pointerType: "mouse"
382
+ });
383
+ return true;
257
384
  }
258
- await page.waitForTimeout?.(150);
259
- return match.label;
385
+ catch {
386
+ return false;
387
+ }
388
+ }
389
+ function filterWorkAxisOptions(items, axis) {
390
+ return items.filter(item => {
391
+ if (isConfigurationAxisRow(item.label))
392
+ return false;
393
+ // Keep every choice axis-specific. This excludes parent actions such as
394
+ // "Reset to default" and prevents a still-open model submenu from being
395
+ // mistaken for effort or speed choices if Escape is unavailable.
396
+ return workAxisOptionLabelMatches(axis, item.label);
397
+ });
398
+ }
399
+ function workAxisOptionLabelMatches(axis, label) {
400
+ if (axis === "model") {
401
+ return /\b(?:gpt[\s-]?\d|sol|luna|terra)\b/i.test(label);
402
+ }
403
+ const candidates = axis === "effort"
404
+ ? [
405
+ ...localeLabels.configurationOptions.light,
406
+ ...localeLabels.configurationOptions.medium,
407
+ ...localeLabels.configurationOptions.high,
408
+ ...localeLabels.configurationOptions.extraHigh,
409
+ ...localeLabels.configurationOptions.max,
410
+ ...localeLabels.configurationOptions.ultra,
411
+ ]
412
+ : axis === "speed"
413
+ ? [
414
+ ...localeLabels.configurationOptions.standard,
415
+ ...localeLabels.configurationOptions.fast,
416
+ ]
417
+ : [];
418
+ return candidates.some(candidate => visibleLabelMatches(label, candidate));
260
419
  }
261
420
  async function selectChatAxis(env, axis, requested, timeoutMs) {
262
421
  const legacyArgs = axis === "modelVersion"
@@ -286,20 +445,46 @@ async function openConfigurationRoot(page, experience) {
286
445
  if (configurationMenuLooksRecognized(existingItems, experience, existing.openerLabel)) {
287
446
  return true;
288
447
  }
448
+ if (existing.openerLabel !== undefined
449
+ && await clickIfUnique(page.getByRole?.("button", { name: existing.openerLabel, exact: true }))) {
450
+ return true;
451
+ }
289
452
  if (typeof page.evaluate === "function") {
290
453
  const clicked = await page.evaluate((surface) => {
291
454
  const normalize = (value) => value.replace(/\s+/g, " ").trim();
292
455
  const visible = (element) => {
293
456
  const html = element;
294
457
  const rect = html.getBoundingClientRect?.();
295
- const style = typeof window !== "undefined" ? window.getComputedStyle?.(html) : undefined;
296
- return (rect === undefined || (rect.width > 0 && rect.height > 0))
297
- && style?.display !== "none"
298
- && style?.visibility !== "hidden"
299
- && style?.opacity !== "0";
458
+ if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
459
+ return false;
460
+ let current = element;
461
+ while (current !== null) {
462
+ if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
463
+ return false;
464
+ }
465
+ const style = typeof window !== "undefined"
466
+ ? window.getComputedStyle?.(current)
467
+ : undefined;
468
+ if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
469
+ return false;
470
+ }
471
+ current = current.parentElement ?? null;
472
+ }
473
+ return true;
300
474
  };
301
- const forms = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i]"));
302
- const controls = forms.flatMap(form => Array.from(form.querySelectorAll("button, [role='button']")))
475
+ const formRoots = Array.from(document.querySelectorAll("main form"));
476
+ const testIdRoots = Array.from(document.querySelectorAll("main [data-testid*='composer' i]"));
477
+ const classRoots = Array.from(document.querySelectorAll("main [class*='composer' i]"));
478
+ const composerRoots = formRoots.length > 0
479
+ ? formRoots
480
+ : testIdRoots.length > 0
481
+ ? testIdRoots
482
+ : classRoots;
483
+ const main = document.querySelector("main");
484
+ const roots = Array.from(new Set(composerRoots.length > 0
485
+ ? composerRoots
486
+ : (main === null ? [] : [main])));
487
+ const controls = Array.from(new Set(roots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button']")))))
303
488
  .filter(visible);
304
489
  const matches = controls.filter(control => {
305
490
  const html = control;
@@ -347,6 +532,9 @@ function configurationMenuLooksRecognized(items, experience, openerLabel) {
347
532
  if (items.some(item => /(?:model|mode|effort|speed)-(?:switcher|selector)|model-switcher/i.test(item.testId ?? ""))) {
348
533
  return true;
349
534
  }
535
+ if (experience === "work" && items.some(item => localeLabels.configurationAxes.advanced.some(label => visibleLabelMatches(item.label, label)))) {
536
+ return true;
537
+ }
350
538
  if (openerLabel === undefined || items.length === 0) {
351
539
  return false;
352
540
  }
@@ -374,13 +562,22 @@ function configurationMenuLooksRecognized(items, experience, openerLabel) {
374
562
  .map(item => normalizeConfigurationId(item.label)));
375
563
  return matched.size >= 2;
376
564
  }
377
- async function ensureWorkConfigurationRoot(page) {
565
+ async function ensureWorkAdvancedPanel(page) {
378
566
  const panel = await readConfigurationPanel(page);
379
567
  if (panel.axisRows.length > 0)
380
568
  return true;
381
- await page.keyboard?.press?.("Escape");
382
- await page.waitForTimeout?.(50);
383
- return openConfigurationRoot(page, "work");
569
+ if (!await openConfigurationRoot(page, "work"))
570
+ return false;
571
+ const reopenedPanel = await readConfigurationPanel(page);
572
+ if (reopenedPanel.axisRows.length > 0)
573
+ return true;
574
+ const items = await enumerateVisibleMenuItems(page);
575
+ const advanced = items.filter(item => localeLabels.configurationAxes.advanced.some(label => visibleLabelMatches(item.label, label)));
576
+ if (advanced.length !== 1 || !await clickVisibleMenuItem(page, advanced[0])) {
577
+ return false;
578
+ }
579
+ await page.waitForTimeout?.(200);
580
+ return (await readConfigurationPanel(page)).axisRows.length > 0;
384
581
  }
385
582
  async function readConfigurationPanel(page) {
386
583
  if (typeof page.evaluate !== "function") {
@@ -395,24 +592,31 @@ async function readConfigurationPanel(page) {
395
592
  const visible = (element) => {
396
593
  const html = element;
397
594
  const rect = html.getBoundingClientRect?.();
398
- const style = typeof window !== "undefined" ? window.getComputedStyle?.(html) : undefined;
399
- return (rect === undefined || (rect.width > 0 && rect.height > 0))
400
- && style?.display !== "none"
401
- && style?.visibility !== "hidden"
402
- && style?.opacity !== "0";
595
+ if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
596
+ return false;
597
+ let current = element;
598
+ while (current !== null) {
599
+ if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
600
+ return false;
601
+ }
602
+ const style = typeof window !== "undefined"
603
+ ? window.getComputedStyle?.(current)
604
+ : undefined;
605
+ if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
606
+ return false;
607
+ }
608
+ current = current.parentElement ?? null;
609
+ }
610
+ return true;
403
611
  };
404
612
  const overlays = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper], [data-radix-menu-content]")).filter(visible);
405
613
  const roots = overlays.length > 0 ? overlays : Array.from(document.querySelectorAll("main")).filter(visible);
406
614
  const rows = roots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option']"))).filter(visible);
407
615
  const axisRows = [];
408
- let advancedVisible = false;
409
616
  for (const row of rows) {
410
617
  const html = row;
411
618
  const label = normalize(row.getAttribute("aria-label") ?? html.innerText ?? row.textContent ?? "");
412
619
  const normalized = label.toLocaleLowerCase();
413
- if (normalizedAxes.advanced?.some(candidate => normalized === candidate || normalized.startsWith(`${candidate} `))) {
414
- advancedVisible = true;
415
- }
416
620
  for (const axis of ["model", "intelligence", "effort", "speed"]) {
417
621
  const candidates = normalizedAxes[axis] ?? [];
418
622
  const prefix = candidates.find(candidate => normalized === candidate || normalized.startsWith(`${candidate} `));
@@ -426,8 +630,19 @@ async function readConfigurationPanel(page) {
426
630
  break;
427
631
  }
428
632
  }
429
- const forms = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i]"));
430
- const openerCandidates = forms.flatMap(form => Array.from(form.querySelectorAll("button, [role='button']")))
633
+ const formRoots = Array.from(document.querySelectorAll("main form"));
634
+ const testIdRoots = Array.from(document.querySelectorAll("main [data-testid*='composer' i]"));
635
+ const classRoots = Array.from(document.querySelectorAll("main [class*='composer' i]"));
636
+ const composerRoots = formRoots.length > 0
637
+ ? formRoots
638
+ : testIdRoots.length > 0
639
+ ? testIdRoots
640
+ : classRoots;
641
+ const main = document.querySelector("main");
642
+ const openerRoots = Array.from(new Set(composerRoots.length > 0
643
+ ? composerRoots
644
+ : (main === null ? [] : [main])));
645
+ const openerCandidates = Array.from(new Set(openerRoots.flatMap(root => Array.from(root.querySelectorAll("button, [role='button']")))))
431
646
  .filter(visible)
432
647
  .map(control => {
433
648
  const html = control;
@@ -441,7 +656,7 @@ async function readConfigurationPanel(page) {
441
656
  || /\b(?:gpt|sol|luna|terra|instant|medium|high|extra high|pro|thinking|extended|light|standard|fast)\b/i.test(item.label));
442
657
  const result = {
443
658
  axisRows,
444
- advancedVisible
659
+ advancedVisible: axisRows.length > 0
445
660
  };
446
661
  if (openerCandidates.length === 1 && openerCandidates[0]?.label.length) {
447
662
  result.openerLabel = openerCandidates[0].label;
@@ -449,44 +664,47 @@ async function readConfigurationPanel(page) {
449
664
  return result;
450
665
  }, localeLabels.configurationAxes).catch(() => ({ axisRows: [], advancedVisible: false }));
451
666
  }
452
- async function clickWorkAxisRow(page, axis) {
667
+ async function findWorkAxisRow(page, axis) {
453
668
  const labels = axis === "modelVersion" ? [] : localeLabels.configurationAxes[axis] ?? [];
454
669
  for (const label of labels) {
455
670
  const pattern = new RegExp(`^${escapeRegExp(label)}(?:\\s|$)`, "i");
456
671
  for (const role of ["button", "menuitem"]) {
457
- if (await clickIfUnique(page.getByRole?.(role, { name: pattern }))) {
458
- return true;
672
+ const locator = page.getByRole?.(role, { name: pattern });
673
+ if (locator?.count !== undefined && await locator.count().catch(() => 0) === 1) {
674
+ return locator;
459
675
  }
460
676
  }
461
677
  }
462
- if (typeof page.evaluate !== "function")
463
- return false;
464
- return page.evaluate((wantedLabels) => {
465
- const normalize = (value) => value.replace(/\s+/g, " ").trim().toLocaleLowerCase();
466
- const wanted = wantedLabels.map(normalize);
467
- const overlays = Array.from(document.querySelectorAll("[role='menu'], [data-radix-popper-content-wrapper], [data-radix-menu-content]"));
468
- const nodes = overlays.flatMap(root => Array.from(root.querySelectorAll("button, [role='button'], [role='menuitem']")));
469
- const matches = nodes.filter(node => {
470
- const html = node;
471
- const label = normalize(node.getAttribute("aria-label") ?? html.innerText ?? node.textContent ?? "");
472
- return wanted.some(prefix => label === prefix || label.startsWith(`${prefix} `));
473
- });
474
- if (matches.length !== 1)
475
- return false;
476
- matches[0].click();
477
- return true;
478
- }, labels).catch(() => false);
678
+ return undefined;
479
679
  }
480
680
  async function clickVisibleMenuItem(page, item) {
481
681
  if (item.testId !== undefined && await clickIfUnique(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`))) {
482
682
  return true;
483
683
  }
484
- for (const role of ["menuitemradio", "menuitem", "option"]) {
684
+ const roles = [...new Set([
685
+ item.role,
686
+ "menuitemradio",
687
+ "menuitem",
688
+ "option"
689
+ ].filter((role) => role !== undefined))];
690
+ // Current Chat can expose visible text such as "Advanced" while assigning
691
+ // the actionable menu item a different accessible name (for example,
692
+ // "Show compact options"). Prefer that enumerated accessible name so the
693
+ // bridge clicks the menu item itself rather than a nested text span.
694
+ if (item.ariaLabel !== undefined) {
695
+ for (const role of roles) {
696
+ if (await clickIfUnique(page.getByRole?.(role, { name: item.ariaLabel, exact: true }))) {
697
+ return true;
698
+ }
699
+ }
700
+ }
701
+ for (const role of roles) {
485
702
  if (await clickIfUnique(page.getByRole?.(role, { name: item.label, exact: true }))) {
486
703
  return true;
487
704
  }
488
705
  }
489
- return clickIfUnique(page.getByText?.(item.label, { exact: true }));
706
+ const exactText = new RegExp(`^\\s*${escapeRegExp(item.label)}\\s*$`, "i");
707
+ return clickIfUnique(page.locator?.("button, [role='button'], [role='menuitem'], [role='menuitemradio'], [role='option']")?.filter?.({ hasText: exactText }));
490
708
  }
491
709
  function findConfigurationOption(items, requested) {
492
710
  const normalizedRequested = normalizeConfigurationId(requested);
@@ -608,16 +826,55 @@ function normalizeConfigurationId(value) {
608
826
  .trim();
609
827
  }
610
828
  async function closeConfigurationMenus(page) {
611
- await page.keyboard?.press?.("Escape");
829
+ if (!await pressConfigurationEscape(page))
830
+ return;
612
831
  await page.waitForTimeout?.(50);
613
- await page.keyboard?.press?.("Escape");
832
+ await pressConfigurationEscape(page);
833
+ }
834
+ async function closeConfigurationSubmenu(page) {
835
+ if (!await pressConfigurationEscape(page))
836
+ return;
837
+ // Radix retains a short pointer-grace timer after a submenu closes. If the
838
+ // next axis is hovered too quickly, that stale timer can dismiss the newly
839
+ // opened submenu. Let the prior close settle before moving to another row.
840
+ await page.waitForTimeout?.(200);
841
+ }
842
+ async function pressConfigurationEscape(page) {
843
+ if (page.keyboard?.press !== undefined) {
844
+ await page.keyboard.press("Escape");
845
+ return true;
846
+ }
847
+ if (page.cua?.keypress !== undefined) {
848
+ try {
849
+ await page.cua.keypress({ keys: ["ESC"] });
850
+ return true;
851
+ }
852
+ catch {
853
+ return false;
854
+ }
855
+ }
856
+ return false;
614
857
  }
615
858
  async function clickIfUnique(locator) {
616
859
  if (locator?.count === undefined || locator.click === undefined)
617
860
  return false;
618
- if (await locator.count().catch(() => 0) !== 1)
861
+ const count = await locator.count().catch(() => 0);
862
+ if (count === 1) {
863
+ await locator.click();
864
+ return true;
865
+ }
866
+ if (count <= 1 || locator.nth === undefined)
867
+ return false;
868
+ const visible = [];
869
+ for (let index = 0; index < count; index += 1) {
870
+ const candidate = locator.nth(index);
871
+ if (candidate.isVisible !== undefined && await candidate.isVisible().catch(() => false)) {
872
+ visible.push(candidate);
873
+ }
874
+ }
875
+ if (visible.length !== 1 || visible[0]?.click === undefined)
619
876
  return false;
620
- await locator.click();
877
+ await visible[0].click();
621
878
  return true;
622
879
  }
623
880
  async function configurationFailure(page, before, desired, selected, message, code, candidates = []) {
@@ -1,2 +1,6 @@
1
1
  import type { CommandContext, PageLike } from "../types.js";
2
- export declare function contextFromPage(page: PageLike | undefined, partial?: Partial<CommandContext>): Promise<CommandContext>;
2
+ export type ContextReadOptions = {
3
+ /** Avoid all optional browser probes on mutation/deadline result paths. */
4
+ minimal?: boolean;
5
+ };
6
+ export declare function contextFromPage(page: PageLike | undefined, partial?: Partial<CommandContext>, options?: ContextReadOptions): Promise<CommandContext>;
@@ -1,8 +1,8 @@
1
1
  import { countPageMessages } from "../dom/messages.js";
2
2
  import { parseConversationId } from "../browser/page-state.js";
3
3
  import { withTimeout } from "./timeouts.js";
4
- export async function contextFromPage(page, partial = {}) {
5
- if (page === undefined) {
4
+ export async function contextFromPage(page, partial = {}, options = {}) {
5
+ if (page === undefined || options.minimal === true) {
6
6
  return { timestamp: new Date().toISOString(), ...partial };
7
7
  }
8
8
  const url = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => partial.url) : partial.url;
@@ -1,17 +1,29 @@
1
1
  import { parseConversationId } from "../browser/page-state.js";
2
+ import { isChatGPTUrl, requireChatGPTUrl } from "../browser/chatgpt-url.js";
2
3
  import { countPageMessages, readLatestMessageText } from "../dom/messages.js";
3
4
  const CHATGPT_HOME = "https://chatgpt.com/";
4
5
  export async function ensureConversationTarget(page, target, options) {
5
6
  const targetUrl = absoluteConversationUrl(target);
7
+ requireChatGPTUrl(targetUrl, "Conversation target URL");
6
8
  const expectedConversationId = parseConversationId(targetUrl);
7
9
  const currentUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
8
- if (expectedConversationId !== undefined
10
+ if (isChatGPTUrl(typeof currentUrl === "string" ? currentUrl : "")
11
+ &&
12
+ expectedConversationId !== undefined
9
13
  && parseConversationId(typeof currentUrl === "string" ? currentUrl : "") === expectedConversationId) {
10
14
  await waitForConversationHydrated(page, options.timeoutMs, expectedConversationId);
11
15
  return ensureResult(false, targetUrl, expectedConversationId);
12
16
  }
13
17
  await page.goto?.(targetUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
18
+ const navigatedUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
19
+ requireChatGPTUrl(typeof navigatedUrl === "string" ? navigatedUrl : "", "Conversation navigation result");
14
20
  await waitForConversationHydrated(page, options.timeoutMs, expectedConversationId);
21
+ const finalUrl = typeof page.url === "function" ? await Promise.resolve(page.url()).catch(() => "") : "";
22
+ requireChatGPTUrl(typeof finalUrl === "string" ? finalUrl : "", "Hydrated conversation URL");
23
+ if (expectedConversationId !== undefined
24
+ && parseConversationId(typeof finalUrl === "string" ? finalUrl : "") !== expectedConversationId) {
25
+ throw new Error(`Visible Chat navigation did not reach conversation ${expectedConversationId}.`);
26
+ }
15
27
  return ensureResult(true, targetUrl, expectedConversationId);
16
28
  }
17
29
  export async function waitForConversationHydrated(page, timeoutMs, expectedConversationId) {