codex-chatgpt-control 0.5.1-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 (204) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/contracts/v1/fixtures/backend-capabilities.json +1 -0
  3. package/contracts/v1/fixtures/command-descriptors.json +21 -0
  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 +8 -1
  19. package/dist/codex-chatgpt-control-backend.mjs +2259 -498
  20. package/dist/codex-chatgpt-control.bundle.mjs +2263 -472
  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/page-state.js +211 -4
  31. package/dist/src/client.d.ts +2 -1
  32. package/dist/src/client.js +2 -1
  33. package/dist/src/commands/configuration.js +46 -12
  34. package/dist/src/commands/context.d.ts +5 -1
  35. package/dist/src/commands/context.js +2 -2
  36. package/dist/src/commands/conversation.js +13 -1
  37. package/dist/src/commands/doctor.js +24 -2
  38. package/dist/src/commands/files.d.ts +1 -0
  39. package/dist/src/commands/files.js +742 -191
  40. package/dist/src/commands/messages.d.ts +2 -1
  41. package/dist/src/commands/messages.js +310 -2
  42. package/dist/src/commands/modes.js +197 -52
  43. package/dist/src/commands/registry.js +3 -0
  44. package/dist/src/commands/sequence.js +3 -1
  45. package/dist/src/commands/session.d.ts +4 -1
  46. package/dist/src/commands/session.js +34 -3
  47. package/dist/src/dom/generation-state.d.ts +6 -1
  48. package/dist/src/dom/generation-state.js +206 -45
  49. package/dist/src/dom/locale/am.d.ts +10 -1
  50. package/dist/src/dom/locale/am.js +10 -1
  51. package/dist/src/dom/locale/ar.d.ts +8 -1
  52. package/dist/src/dom/locale/ar.js +9 -2
  53. package/dist/src/dom/locale/bg.d.ts +6 -1
  54. package/dist/src/dom/locale/bg.js +6 -1
  55. package/dist/src/dom/locale/bn.d.ts +10 -1
  56. package/dist/src/dom/locale/bn.js +10 -1
  57. package/dist/src/dom/locale/bs.d.ts +5 -1
  58. package/dist/src/dom/locale/bs.js +5 -1
  59. package/dist/src/dom/locale/ca.d.ts +8 -1
  60. package/dist/src/dom/locale/ca.js +8 -1
  61. package/dist/src/dom/locale/cs.d.ts +5 -1
  62. package/dist/src/dom/locale/cs.js +6 -2
  63. package/dist/src/dom/locale/da.d.ts +5 -1
  64. package/dist/src/dom/locale/da.js +6 -2
  65. package/dist/src/dom/locale/de.d.ts +4 -1
  66. package/dist/src/dom/locale/de.js +5 -2
  67. package/dist/src/dom/locale/el.d.ts +5 -1
  68. package/dist/src/dom/locale/el.js +6 -2
  69. package/dist/src/dom/locale/en.d.ts +2 -1
  70. package/dist/src/dom/locale/en.js +2 -1
  71. package/dist/src/dom/locale/es-419.d.ts +5 -1
  72. package/dist/src/dom/locale/es-419.js +5 -1
  73. package/dist/src/dom/locale/es-ES.d.ts +5 -1
  74. package/dist/src/dom/locale/es-ES.js +5 -1
  75. package/dist/src/dom/locale/et.d.ts +4 -1
  76. package/dist/src/dom/locale/et.js +5 -2
  77. package/dist/src/dom/locale/fa.d.ts +9 -1
  78. package/dist/src/dom/locale/fa.js +10 -2
  79. package/dist/src/dom/locale/fi.d.ts +5 -1
  80. package/dist/src/dom/locale/fi.js +6 -2
  81. package/dist/src/dom/locale/fr-CA.d.ts +4 -1
  82. package/dist/src/dom/locale/fr-CA.js +4 -1
  83. package/dist/src/dom/locale/fr-FR.d.ts +5 -1
  84. package/dist/src/dom/locale/fr-FR.js +7 -3
  85. package/dist/src/dom/locale/gu.d.ts +10 -1
  86. package/dist/src/dom/locale/gu.js +10 -1
  87. package/dist/src/dom/locale/hi.d.ts +10 -1
  88. package/dist/src/dom/locale/hi.js +11 -2
  89. package/dist/src/dom/locale/hr.d.ts +4 -1
  90. package/dist/src/dom/locale/hr.js +4 -1
  91. package/dist/src/dom/locale/hu.d.ts +4 -1
  92. package/dist/src/dom/locale/hu.js +5 -2
  93. package/dist/src/dom/locale/hy.d.ts +10 -1
  94. package/dist/src/dom/locale/hy.js +11 -2
  95. package/dist/src/dom/locale/id.d.ts +5 -1
  96. package/dist/src/dom/locale/id.js +5 -1
  97. package/dist/src/dom/locale/index.js +1 -0
  98. package/dist/src/dom/locale/is.d.ts +9 -1
  99. package/dist/src/dom/locale/is.js +10 -2
  100. package/dist/src/dom/locale/it.d.ts +4 -1
  101. package/dist/src/dom/locale/it.js +6 -3
  102. package/dist/src/dom/locale/ja.d.ts +6 -1
  103. package/dist/src/dom/locale/ja.js +7 -2
  104. package/dist/src/dom/locale/ka.d.ts +10 -1
  105. package/dist/src/dom/locale/ka.js +11 -2
  106. package/dist/src/dom/locale/kk.d.ts +10 -1
  107. package/dist/src/dom/locale/kk.js +11 -2
  108. package/dist/src/dom/locale/kn.d.ts +9 -1
  109. package/dist/src/dom/locale/kn.js +10 -2
  110. package/dist/src/dom/locale/ko.d.ts +6 -1
  111. package/dist/src/dom/locale/ko.js +7 -2
  112. package/dist/src/dom/locale/lt.d.ts +5 -1
  113. package/dist/src/dom/locale/lt.js +6 -2
  114. package/dist/src/dom/locale/lv.d.ts +5 -1
  115. package/dist/src/dom/locale/lv.js +5 -1
  116. package/dist/src/dom/locale/mk.d.ts +9 -1
  117. package/dist/src/dom/locale/mk.js +10 -2
  118. package/dist/src/dom/locale/ml.d.ts +10 -1
  119. package/dist/src/dom/locale/ml.js +10 -1
  120. package/dist/src/dom/locale/mn.d.ts +10 -1
  121. package/dist/src/dom/locale/mn.js +13 -4
  122. package/dist/src/dom/locale/mr.d.ts +10 -1
  123. package/dist/src/dom/locale/mr.js +10 -1
  124. package/dist/src/dom/locale/ms.d.ts +9 -1
  125. package/dist/src/dom/locale/ms.js +9 -1
  126. package/dist/src/dom/locale/my.d.ts +9 -1
  127. package/dist/src/dom/locale/my.js +9 -1
  128. package/dist/src/dom/locale/nb.d.ts +5 -1
  129. package/dist/src/dom/locale/nb.js +6 -2
  130. package/dist/src/dom/locale/nl.d.ts +8 -2
  131. package/dist/src/dom/locale/nl.js +8 -2
  132. package/dist/src/dom/locale/pa.d.ts +10 -1
  133. package/dist/src/dom/locale/pa.js +10 -1
  134. package/dist/src/dom/locale/pl.d.ts +5 -1
  135. package/dist/src/dom/locale/pl.js +6 -2
  136. package/dist/src/dom/locale/pt-BR.d.ts +5 -1
  137. package/dist/src/dom/locale/pt-BR.js +6 -2
  138. package/dist/src/dom/locale/pt-PT.d.ts +5 -1
  139. package/dist/src/dom/locale/pt-PT.js +5 -1
  140. package/dist/src/dom/locale/ro.d.ts +4 -1
  141. package/dist/src/dom/locale/ro.js +4 -1
  142. package/dist/src/dom/locale/ru.d.ts +8 -1
  143. package/dist/src/dom/locale/ru.js +9 -2
  144. package/dist/src/dom/locale/sk.d.ts +5 -1
  145. package/dist/src/dom/locale/sk.js +5 -1
  146. package/dist/src/dom/locale/sl.d.ts +9 -1
  147. package/dist/src/dom/locale/sl.js +9 -1
  148. package/dist/src/dom/locale/so.d.ts +9 -1
  149. package/dist/src/dom/locale/so.js +9 -1
  150. package/dist/src/dom/locale/sq.d.ts +10 -1
  151. package/dist/src/dom/locale/sq.js +12 -3
  152. package/dist/src/dom/locale/sr.d.ts +9 -1
  153. package/dist/src/dom/locale/sr.js +10 -2
  154. package/dist/src/dom/locale/sv.d.ts +4 -1
  155. package/dist/src/dom/locale/sv.js +4 -1
  156. package/dist/src/dom/locale/sw.d.ts +5 -1
  157. package/dist/src/dom/locale/sw.js +7 -3
  158. package/dist/src/dom/locale/ta.d.ts +10 -1
  159. package/dist/src/dom/locale/ta.js +12 -3
  160. package/dist/src/dom/locale/te.d.ts +10 -1
  161. package/dist/src/dom/locale/te.js +11 -2
  162. package/dist/src/dom/locale/th.d.ts +6 -1
  163. package/dist/src/dom/locale/th.js +6 -1
  164. package/dist/src/dom/locale/tl.d.ts +1 -1
  165. package/dist/src/dom/locale/tl.js +2 -2
  166. package/dist/src/dom/locale/tr.d.ts +9 -1
  167. package/dist/src/dom/locale/tr.js +10 -2
  168. package/dist/src/dom/locale/types.d.ts +1 -1
  169. package/dist/src/dom/locale/uk.d.ts +10 -1
  170. package/dist/src/dom/locale/uk.js +11 -2
  171. package/dist/src/dom/locale/ur.d.ts +9 -1
  172. package/dist/src/dom/locale/ur.js +9 -1
  173. package/dist/src/dom/locale/vi.d.ts +9 -1
  174. package/dist/src/dom/locale/vi.js +10 -2
  175. package/dist/src/dom/locale/zh-HK.d.ts +8 -1
  176. package/dist/src/dom/locale/zh-HK.js +9 -2
  177. package/dist/src/dom/locale/zh-Hans.d.ts +10 -1
  178. package/dist/src/dom/locale/zh-Hans.js +11 -2
  179. package/dist/src/dom/locale/zh-TW.d.ts +10 -1
  180. package/dist/src/dom/locale/zh-TW.js +11 -2
  181. package/dist/src/dom/selectors.d.ts +1 -0
  182. package/dist/src/dom/selectors.js +9 -1
  183. package/dist/src/dom/wait-snapshot.d.ts +1 -0
  184. package/dist/src/dom/wait-snapshot.js +45 -22
  185. package/dist/src/errors.d.ts +1 -1
  186. package/dist/src/errors.js +2 -2
  187. package/dist/src/safety/risk.d.ts +1 -0
  188. package/dist/src/safety/risk.js +1 -0
  189. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +2 -2
  190. package/dist/src/scripts/apply-intelligence-locale-captures.js +54 -14
  191. package/dist/src/scripts/capture-intelligence-locales.d.ts +114 -1
  192. package/dist/src/scripts/capture-intelligence-locales.js +437 -304
  193. package/dist/src/scripts/live-smoke/scenarios.d.ts +2 -1
  194. package/dist/src/scripts/live-smoke/scenarios.js +9 -10
  195. package/dist/src/scripts/locale-capture/surface-graph.d.ts +7 -2
  196. package/dist/src/scripts/locale-capture/surface-graph.js +7 -0
  197. package/dist/src/types.d.ts +27 -6
  198. package/package.json +2 -1
  199. package/references/2026-08-08-model-selection-locale-sweep.md +32 -0
  200. package/references/backend-protocol.md +11 -3
  201. package/references/language-coverage.md +14 -14
  202. package/references/localization.md +17 -13
  203. package/references/python-parity.md +8 -2
  204. package/references/troubleshooting.md +10 -2
@@ -63,7 +63,8 @@ export async function setMode(env, args) {
63
63
  return selectorDrift(page, "No unique ChatGPT mode menu opener was found.");
64
64
  }
65
65
  await page.waitForTimeout?.(250);
66
- const candidates = await enumerateVisibleMenuItems(page);
66
+ let candidates = await enumerateVisibleMenuItems(page);
67
+ const observedCandidates = [...candidates];
67
68
  const selected = [];
68
69
  if (requested.length > 0 && shouldRejectAsWrongModeMenu(candidates)) {
69
70
  const candidateLabels = candidates.map(candidate => candidate.label);
@@ -76,9 +77,20 @@ export async function setMode(env, args) {
76
77
  };
77
78
  }
78
79
  for (const request of requested) {
79
- const match = findModeMenuItem(candidates, request);
80
+ let match = findModeMenuItem(candidates, request);
81
+ if (match === undefined) {
82
+ const sliderSelection = await selectModeWithPowerSlider(page, request);
83
+ if (sliderSelection !== undefined) {
84
+ selected.push(sliderSelection);
85
+ continue;
86
+ }
87
+ const nested = await openEffortSubmenu(page, candidates, request);
88
+ observedCandidates.push(...nested);
89
+ candidates = nested;
90
+ match = findModeMenuItem(candidates, request);
91
+ }
80
92
  if (match === undefined) {
81
- const candidateLabels = candidates.map(candidate => candidate.label);
93
+ const candidateLabels = dedupeLabels(observedCandidates.map(candidate => candidate.label));
82
94
  return {
83
95
  ok: false,
84
96
  status: "unsupported",
@@ -87,12 +99,12 @@ export async function setMode(env, args) {
87
99
  context: await contextFromPage(page)
88
100
  };
89
101
  }
90
- if (!await clickMenuItem(page, match.label)) {
102
+ if (!await clickResolvedMenuItem(page, match)) {
91
103
  return selectorDrift(page, `Mode option "${match.label}" was visible but could not be clicked.`, candidates.map(candidate => candidate.label));
92
104
  }
93
105
  selected.push(match.label);
94
106
  }
95
- let candidateLabels = candidates.map(candidate => candidate.label);
107
+ let candidateLabels = dedupeLabels(observedCandidates.map(candidate => candidate.label));
96
108
  if (requestedVersion !== undefined) {
97
109
  const versionResult = await selectModelVersion(page, requestedVersion, candidates, args.timeoutMs ?? 30000);
98
110
  candidateLabels = dedupeLabels([...candidateLabels, ...versionResult.candidates]);
@@ -114,6 +126,87 @@ export async function setMode(env, args) {
114
126
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
115
127
  }
116
128
  }
129
+ /**
130
+ * Chat's compact picker exposes the five canonical intelligence levels as an
131
+ * ARIA slider. Use it only when its visible min/max/current metadata exactly
132
+ * matches that five-step scale, and accept the move only when the composer
133
+ * visibly echoes the requested label. The Advanced submenu remains the
134
+ * fallback for every unrecognized shape.
135
+ */
136
+ async function selectModeWithPowerSlider(page, request) {
137
+ const wanted = request.modeId === undefined
138
+ ? undefined
139
+ : CANONICAL_INTELLIGENCE_ORDER.get(request.modeId);
140
+ const slider = page.locator?.("[role='slider'][aria-valuemin='0'][aria-valuemax='4']");
141
+ if (wanted === undefined
142
+ || slider?.count === undefined
143
+ || slider.evaluate === undefined
144
+ || slider.press === undefined
145
+ || slider.isVisible === undefined
146
+ || !await slider.isVisible().catch(() => false)
147
+ || await slider.count().catch(() => 0) !== 1) {
148
+ return undefined;
149
+ }
150
+ const state = await slider.evaluate(element => ({
151
+ min: Number(element.getAttribute("aria-valuemin")),
152
+ max: Number(element.getAttribute("aria-valuemax")),
153
+ now: Number(element.getAttribute("aria-valuenow"))
154
+ })).catch(() => undefined);
155
+ if (state === undefined
156
+ || state.min !== 0
157
+ || state.max !== CANONICAL_INTELLIGENCE_ORDER.size - 1
158
+ || !Number.isInteger(state.now)
159
+ || state.now < state.min
160
+ || state.now > state.max) {
161
+ return undefined;
162
+ }
163
+ const key = wanted > state.now ? "ArrowRight" : "ArrowLeft";
164
+ for (let step = 0; step < Math.abs(wanted - state.now); step += 1) {
165
+ await slider.press(key);
166
+ }
167
+ await page.waitForTimeout?.(150);
168
+ return findUniqueVisibleLabelForRequest(await visibleModeButtonLabelList(page), request);
169
+ }
170
+ /**
171
+ * Current Chat exposes the intelligence choices inside an Advanced "Effort <value>"
172
+ * submenu. Only traverse a uniquely labelled visible Effort/Intelligence row and
173
+ * only accept the submenu when it contains the exact requested mode. This keeps the
174
+ * legacy flat picker working while avoiding any inference from the five-position
175
+ * power slider.
176
+ */
177
+ async function openEffortSubmenu(page, rootItems, request) {
178
+ const axisLabels = [
179
+ ...localeLabels.configurationAxes.effort,
180
+ ...localeLabels.configurationAxes.intelligence,
181
+ ].map(normalizeForLabelMatch);
182
+ const effortRows = (items) => items.filter(item => {
183
+ if (item.role === "menuitemradio")
184
+ return false;
185
+ const normalized = normalizeForLabelMatch(item.label);
186
+ return axisLabels.some(axis => normalized === axis || normalized.startsWith(`${axis} `));
187
+ });
188
+ let visibleRootItems = rootItems;
189
+ let rows = effortRows(visibleRootItems);
190
+ if (rows.length === 0) {
191
+ const advancedLabels = localeLabels.configurationAxes.advanced.map(normalizeForLabelMatch);
192
+ const advancedRows = visibleRootItems.filter(item => {
193
+ const normalized = normalizeForLabelMatch(item.label);
194
+ return advancedLabels.some(label => normalized === label || visibleLabelMatches(item.label, label));
195
+ });
196
+ if (advancedRows.length !== 1 || !await clickResolvedMenuItem(page, advancedRows[0])) {
197
+ return [];
198
+ }
199
+ await page.waitForTimeout?.(250);
200
+ visibleRootItems = await enumerateVisibleMenuItems(page);
201
+ rows = effortRows(visibleRootItems);
202
+ }
203
+ if (rows.length !== 1 || !await clickResolvedMenuItem(page, rows[0])) {
204
+ return [];
205
+ }
206
+ await page.waitForTimeout?.(250);
207
+ const nested = await enumerateVisibleMenuItems(page);
208
+ return findModeMenuItem(nested, request) === undefined ? [] : nested;
209
+ }
117
210
  /**
118
211
  * Post-condition check: after clicking mode rows, the composer's mode-labelled controls
119
212
  * should reflect the requested selection. A mismatch does not fail the command — some
@@ -216,7 +309,7 @@ export async function selectTool(env, args) {
216
309
  context: await contextFromPage(page)
217
310
  };
218
311
  }
219
- if (!await clickMenuItem(page, match.label)) {
312
+ if (!await clickMenuItem(page, match)) {
220
313
  return selectorDrift(page, `Tool "${match.label}" was visible but could not be clicked.`, candidates.map(candidate => candidate.label));
221
314
  }
222
315
  return resultOk({ selected: match.label, candidates: candidates.map(candidate => candidate.label) }, await contextFromPage(page));
@@ -295,25 +388,24 @@ function shouldRejectAsWrongModeMenu(items) {
295
388
  }
296
389
  return items.some(item => isThreadActionLabel(item.label));
297
390
  }
298
- async function clickMenuItem(page, label) {
299
- if (await clickModelSwitcherMenuItem(page, label)) {
391
+ async function clickMenuItem(page, item) {
392
+ if (await clickModelSwitcherMenuItem(page, item)) {
300
393
  return true;
301
394
  }
302
- if (await clickMenuItemByPointer(page, label)) {
395
+ if (await clickMenuItemByPointer(page, item)) {
303
396
  return true;
304
397
  }
305
- if (await clickMenuItemByDom(page, label)) {
306
- return true;
307
- }
308
- const roleLocator = page.locator?.("[role='menuitem'], [role='menuitemradio'], [role='option']")?.filter?.({ hasText: label });
309
- if (await clickIfUnique(roleLocator)) {
310
- return true;
398
+ if (item.role !== undefined) {
399
+ const accessibleName = item.ariaLabel ?? item.label;
400
+ if (await clickIfUniqueMenuControl(page.getByRole?.(item.role, { name: accessibleName, exact: true }), item))
401
+ return true;
311
402
  }
312
- const textLocator = page.getByText?.(label, { exact: true });
313
- return clickIfUnique(textLocator);
403
+ const roleLocator = page.locator?.("[role='menuitem'], [role='menuitemradio'], [role='option']")
404
+ ?.filter?.({ hasText: new RegExp(`^\\s*${escapeRegExp(item.label)}\\s*$`, "i") });
405
+ return clickIfUniqueMenuControl(roleLocator, item);
314
406
  }
315
- async function clickMenuItemByPointer(page, label) {
316
- const point = await menuItemCenter(page, { label });
407
+ async function clickMenuItemByPointer(page, item) {
408
+ const point = await menuItemCenter(page, item);
317
409
  if (point === undefined) {
318
410
  return false;
319
411
  }
@@ -328,12 +420,12 @@ async function clickMenuItemByPointer(page, label) {
328
420
  }
329
421
  return false;
330
422
  }
331
- async function clickModelSwitcherMenuItem(page, label) {
423
+ async function clickModelSwitcherMenuItem(page, item) {
332
424
  if (typeof page.evaluate !== "function" || typeof page.locator !== "function") {
333
425
  return false;
334
426
  }
335
427
  const testId = await page.evaluate((wanted) => {
336
- const normalizedWanted = wanted.replace(/\s+/g, " ").trim().toLowerCase();
428
+ const normalizedWanted = wanted.label.replace(/\s+/g, " ").trim().toLowerCase();
337
429
  const candidates = Array.from(document.querySelectorAll("[data-testid^='model-switcher-']"));
338
430
  const matches = candidates
339
431
  .filter(node => {
@@ -341,35 +433,19 @@ async function clickModelSwitcherMenuItem(page, label) {
341
433
  const candidateTestId = element.getAttribute("data-testid") ?? "";
342
434
  if (candidateTestId.endsWith("-effort"))
343
435
  return false;
436
+ if (wanted.testId !== undefined && candidateTestId !== wanted.testId)
437
+ return false;
344
438
  const text = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim().toLowerCase();
345
439
  return text === normalizedWanted;
346
440
  })
347
441
  .map(node => node.getAttribute("data-testid"))
348
442
  .filter((value) => value !== null);
349
443
  return matches.length === 1 ? matches[0] : undefined;
350
- }, label).catch(() => undefined);
444
+ }, item.testId === undefined ? { label: item.label } : { label: item.label, testId: item.testId }).catch(() => undefined);
351
445
  if (testId === undefined) {
352
446
  return false;
353
447
  }
354
- return clickIfUnique(page.locator(`[data-testid="${escapeAttributeValue(testId)}"]`));
355
- }
356
- async function clickMenuItemByDom(page, label) {
357
- if (typeof page.evaluate !== "function") {
358
- return false;
359
- }
360
- return page.evaluate((wanted) => {
361
- const normalizedWanted = wanted.replace(/\s+/g, " ").trim().toLowerCase();
362
- const candidates = Array.from(document.querySelectorAll("[role='menuitem'], [role='menuitemradio'], [role='option']"));
363
- const matches = candidates.filter(node => {
364
- const element = node;
365
- const text = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim().toLowerCase();
366
- return text === normalizedWanted;
367
- });
368
- if (matches.length !== 1)
369
- return false;
370
- matches[0].click();
371
- return true;
372
- }, label).catch(() => false);
448
+ return clickIfUniqueMenuControl(page.locator(`[data-testid="${escapeAttributeValue(testId)}"]`), item);
373
449
  }
374
450
  async function clickIfUnique(locator) {
375
451
  if (locator === undefined || typeof locator.count !== "function" || typeof locator.click !== "function") {
@@ -382,6 +458,46 @@ async function clickIfUnique(locator) {
382
458
  await locator.click();
383
459
  return true;
384
460
  }
461
+ async function clickIfUniqueMenuControl(locator, item) {
462
+ if (locator === undefined
463
+ || typeof locator.count !== "function"
464
+ || typeof locator.evaluate !== "function"
465
+ || typeof locator.click !== "function") {
466
+ return false;
467
+ }
468
+ if (await locator.count().catch(() => 0) !== 1)
469
+ return false;
470
+ const safe = await locator.evaluate((element) => {
471
+ const control = element;
472
+ if (control.disabled || control.getAttribute("aria-disabled") === "true")
473
+ return false;
474
+ if (control.hidden || control.closest("[hidden], [inert], [aria-hidden='true']") !== null)
475
+ return false;
476
+ const style = window.getComputedStyle(control);
477
+ const rect = control.getBoundingClientRect();
478
+ if (rect.width <= 0 || rect.height <= 0
479
+ || style.display === "none"
480
+ || style.visibility === "hidden"
481
+ || style.opacity === "0"
482
+ || style.pointerEvents === "none")
483
+ return false;
484
+ const containers = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]")).filter(container => {
485
+ if (container.hidden || container.closest("[hidden], [inert], [aria-hidden='true']") !== null)
486
+ return false;
487
+ const containerStyle = window.getComputedStyle(container);
488
+ const containerRect = container.getBoundingClientRect();
489
+ return containerRect.width > 0 && containerRect.height > 0
490
+ && containerStyle.display !== "none"
491
+ && containerStyle.visibility !== "hidden"
492
+ && containerStyle.opacity !== "0";
493
+ });
494
+ return containers.length > 0 && containers.some(container => container.contains(control));
495
+ }).catch(() => false);
496
+ if (!safe)
497
+ return false;
498
+ await locator.click();
499
+ return true;
500
+ }
385
501
  function toolLabels(tool) {
386
502
  const known = localeLabels.tools[tool];
387
503
  return known !== undefined ? [...known] : [tool];
@@ -524,13 +640,13 @@ function isModelVersionSubmenuOpener(item) {
524
640
  || (item.role !== "menuitemradio" && MODEL_VERSION_FAMILY_PATTERN.test(item.label));
525
641
  }
526
642
  async function clickResolvedMenuItem(page, item) {
527
- if (item.testId !== undefined && await clickIfUnique(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`))) {
643
+ if (item.testId !== undefined && await clickIfUniqueMenuControl(page.locator?.(`[data-testid="${escapeAttributeValue(item.testId)}"]`), item)) {
528
644
  return true;
529
645
  }
530
- if (item.role !== undefined && await clickIfUnique(page.getByRole?.(item.role, { name: item.label, exact: true }))) {
646
+ if (item.role !== undefined && await clickIfUniqueMenuControl(page.getByRole?.(item.role, { name: item.ariaLabel ?? item.label, exact: true }), item)) {
531
647
  return true;
532
648
  }
533
- return clickMenuItem(page, item.label);
649
+ return clickMenuItem(page, item);
534
650
  }
535
651
  async function openModelVersionSubmenu(page, candidates) {
536
652
  const submenuOpeners = candidates.filter(item => item.hasPopup === true || MODEL_VERSION_FAMILY_PATTERN.test(item.label));
@@ -544,7 +660,7 @@ async function openModelVersionSubmenu(page, candidates) {
544
660
  return true;
545
661
  }
546
662
  }
547
- if (await clickMenuItem(page, candidate.label)) {
663
+ if (await clickMenuItem(page, candidate)) {
548
664
  await page.waitForTimeout?.(250);
549
665
  if (await modelVersionMenuItemsAreVisible(page)) {
550
666
  return true;
@@ -581,23 +697,34 @@ async function menuItemCenter(page, item, roles = ["menuitem", "menuitemradio",
581
697
  const normalize = (value) => value.replace(/\s+/g, " ").trim().toLowerCase();
582
698
  const normalizedLabel = normalize(target.label);
583
699
  const roleSelector = target.roles.map(role => `[role='${role}']`).join(",");
700
+ const visible = (element) => {
701
+ if (element.hidden || element.closest("[hidden], [inert], [aria-hidden='true']") !== null)
702
+ return false;
703
+ const rect = element.getBoundingClientRect();
704
+ const style = window.getComputedStyle(element);
705
+ return rect.width > 0
706
+ && rect.height > 0
707
+ && style.visibility !== "hidden"
708
+ && style.display !== "none"
709
+ && style.opacity !== "0"
710
+ && style.pointerEvents !== "none";
711
+ };
712
+ const containers = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]")).filter(visible);
584
713
  const matches = Array.from(document.querySelectorAll(roleSelector))
585
714
  .filter(node => {
586
715
  const element = node;
587
716
  if (target.testId !== undefined && element.getAttribute("data-testid") !== target.testId) {
588
717
  return false;
589
718
  }
719
+ if (element.disabled || element.getAttribute("aria-disabled") === "true")
720
+ return false;
590
721
  const label = normalize(element.innerText ?? element.textContent ?? "");
591
722
  if (label !== normalizedLabel) {
592
723
  return false;
593
724
  }
594
- const rect = element.getBoundingClientRect();
595
- const style = window.getComputedStyle(element);
596
- return rect.width > 0
597
- && rect.height > 0
598
- && style.visibility !== "hidden"
599
- && style.display !== "none"
600
- && style.opacity !== "0";
725
+ return visible(element)
726
+ && containers.length > 0
727
+ && containers.some(container => container.contains(element));
601
728
  });
602
729
  if (matches.length !== 1)
603
730
  return undefined;
@@ -679,6 +806,24 @@ async function visibleModeButtonLabelList(page) {
679
806
  return Array.from(document.querySelectorAll("button, [role='button']"))
680
807
  .map(node => {
681
808
  const element = node;
809
+ if (element.hidden
810
+ || element.getAttribute("aria-hidden") === "true"
811
+ || (typeof element.closest === "function"
812
+ && element.closest("[hidden], [inert], [aria-hidden='true']") !== null))
813
+ return "";
814
+ const style = typeof window?.getComputedStyle === "function"
815
+ ? window.getComputedStyle(element)
816
+ : undefined;
817
+ if (style?.display === "none"
818
+ || style?.visibility === "hidden"
819
+ || style?.opacity === "0"
820
+ || style?.pointerEvents === "none")
821
+ return "";
822
+ const rect = typeof element.getBoundingClientRect === "function"
823
+ ? element.getBoundingClientRect()
824
+ : { width: 1, height: 1 };
825
+ if (rect.width <= 0 && rect.height <= 0)
826
+ return "";
682
827
  if (scopedRoots.length > 0 && !scopedRoots.some(root => root.contains(node)))
683
828
  return "";
684
829
  const visibleText = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
@@ -84,6 +84,7 @@ const descriptors = [
84
84
  primitive("messages.wait", "Wait for the latest assistant response to stabilize.", 120000),
85
85
  primitive("messages.readLatest", "Read the latest message as Markdown, normalized text, blocks, or HTML.", 30000),
86
86
  primitive("messages.status", "Read a compact latest-assistant progress snapshot without treating partial text as complete.", 5000),
87
+ primitive("messages.stop", "Stop the current visible ChatGPT response after explicit confirmation and verify generation became inactive.", 5000),
87
88
  primitive("messages.waitAndRead", "Wait for completion and read the latest message.", 120000),
88
89
  primitive("artifacts.listLatest", "Detect the latest visible generated ChatGPT artifact, such as an image-only result.", 30000),
89
90
  primitive("artifacts.wait", "Wait for a visible generated ChatGPT artifact to appear and stabilize.", 120000),
@@ -273,6 +274,8 @@ function primitiveArgs(name) {
273
274
  return { role: "assistant or user", format: "markdown, normalized_text, visible_text, html, blocks, or all" };
274
275
  if (name === "messages.status")
275
276
  return { maxPreviewChars: "maximum latest-assistant preview characters to return; defaults to 240" };
277
+ if (name === "messages.stop")
278
+ return { confirmStop: "must be true to stop a visible response", timeoutMs: "maximum time to verify generation became inactive" };
276
279
  if (name === "experience.detect")
277
280
  return { timeoutMs: "optional timeout for attaching to the ChatGPT page" };
278
281
  if (name === "experience.open")
@@ -3,7 +3,7 @@ import { applyConfiguration, inspectConfiguration } from "./configuration.js";
3
3
  import { detectExperience, openExperience } from "./experience.js";
4
4
  import { attachFiles, downloadLatestFile } from "./files.js";
5
5
  import { addProjectSources, buildProjectSourceAddPlan, listProjectSources } from "./project-sources.js";
6
- import { askMessage, composeMessage, messageStatus, readLatest, submitMessage, waitAndRead, waitForMessage } from "./messages.js";
6
+ import { askMessage, composeMessage, messageStatus, readLatest, stopGeneration, submitMessage, waitAndRead, waitForMessage } from "./messages.js";
7
7
  import { copyResponse } from "./response-actions.js";
8
8
  import { bootstrap } from "./session.js";
9
9
  import { newThread, openThread, searchThreads } from "./threads.js";
@@ -82,6 +82,8 @@ export async function executeStep(step, env, previousResults) {
82
82
  return readLatest(env, step.args);
83
83
  case "messages.status":
84
84
  return messageStatus(env, step.args);
85
+ case "messages.stop":
86
+ return stopGeneration(env, step.args);
85
87
  case "messages.waitAndRead":
86
88
  return waitAndRead(env, step.args);
87
89
  case "artifacts.listLatest":
@@ -1,4 +1,7 @@
1
1
  import type { BootstrapArgs, BootstrapData, CommandResult, RuntimeEnv } from "../types.js";
2
+ export type EnsurePageOptions = {
3
+ minimalContext?: boolean;
4
+ };
2
5
  export declare function bootstrap(env: RuntimeEnv, args?: BootstrapArgs): Promise<CommandResult<BootstrapData>>;
3
- export declare function ensurePage(env: RuntimeEnv): Promise<CommandResult<unknown>>;
6
+ export declare function ensurePage(env: RuntimeEnv, options?: EnsurePageOptions): Promise<CommandResult<unknown>>;
4
7
  export declare function verifyTabAffinity(env: RuntimeEnv): Promise<CommandResult<unknown> | undefined>;
@@ -1,4 +1,4 @@
1
- import { attachChatGPTBrowser, tabIdFromPage } from "../browser/attach.js";
1
+ import { attachChatGPTBrowser, isChatGPTUrl, tabIdFromPage } from "../browser/attach.js";
2
2
  import { readPageState } from "../browser/page-state.js";
3
3
  import { resultError, resultOk } from "../errors.js";
4
4
  import { contextFromPage } from "./context.js";
@@ -26,7 +26,7 @@ export async function bootstrap(env, args = {}) {
26
26
  return resultError(error instanceof Error ? error : new Error(String(error)));
27
27
  }
28
28
  }
29
- export async function ensurePage(env) {
29
+ export async function ensurePage(env, options = {}) {
30
30
  if (env.page === undefined) {
31
31
  return bootstrap(env, { preferExistingTab: true });
32
32
  }
@@ -34,7 +34,38 @@ export async function ensurePage(env) {
34
34
  if (affinity !== undefined) {
35
35
  return affinity;
36
36
  }
37
- return resultOk({}, await contextFromPage(env.page, tabContext(env)));
37
+ const origin = await verifyChatGPTOrigin(env);
38
+ if (origin !== undefined) {
39
+ return origin;
40
+ }
41
+ return resultOk({}, await contextFromPage(env.page, tabContext(env), { minimal: options.minimalContext === true }));
42
+ }
43
+ async function verifyChatGPTOrigin(env) {
44
+ if (env.page === undefined)
45
+ return undefined;
46
+ const actualUrl = await Promise.resolve(env.page.url?.()).catch(() => undefined);
47
+ if (isChatGPTUrl(actualUrl))
48
+ return undefined;
49
+ return {
50
+ ok: false,
51
+ status: "blocked",
52
+ warnings: [],
53
+ blocker: {
54
+ kind: "selector_drift",
55
+ code: "unsafe_chatgpt_origin",
56
+ message: "ChatGPT command refused to operate because the controlled tab is not on an allowlisted ChatGPT origin.",
57
+ visibleText: actualUrl ?? "The current tab URL could not be verified.",
58
+ remediation: [
59
+ {
60
+ label: "Reopen ChatGPT",
61
+ instruction: "Run session.bootstrap against https://chatgpt.com or claim an exact supported ChatGPT tab before retrying.",
62
+ userActionRequired: false
63
+ }
64
+ ],
65
+ resumable: false
66
+ },
67
+ context: await contextFromPage(env.page, tabContext(env))
68
+ };
38
69
  }
39
70
  export async function verifyTabAffinity(env) {
40
71
  if (env.expectedTabId === undefined || env.page === undefined) {
@@ -1,5 +1,10 @@
1
1
  import type { PageLike } from "../types.js";
2
+ export type GenerationStateReadOptions = {
3
+ timeoutMs?: number;
4
+ };
2
5
  export type AssistantGenerationState = {
6
+ /** True only when the page DOM was inspected successfully. */
7
+ observed: boolean;
3
8
  active: boolean;
4
9
  stopped: boolean;
5
10
  signals: string[];
@@ -11,5 +16,5 @@ export type AssistantGenerationState = {
11
16
  * response is complete.
12
17
  */
13
18
  export declare const EMPTY_GENERATION_STATE: AssistantGenerationState;
14
- export declare function readAssistantGenerationState(page: PageLike): Promise<AssistantGenerationState>;
19
+ export declare function readAssistantGenerationState(page: PageLike, options?: GenerationStateReadOptions): Promise<AssistantGenerationState>;
15
20
  export declare function latestAssistantTurnHasResponseActions(page: PageLike): Promise<boolean>;