codex-chatgpt-control 0.2.0-alpha.1 → 0.5.0-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 (233) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +42 -2
  3. package/contracts/v1/fixtures/backend-capabilities.json +11 -0
  4. package/contracts/v1/fixtures/command-descriptors.json +289 -7
  5. package/contracts/v1/fixtures/describe-runner-run.json +5 -2
  6. package/contracts/v1/fixtures/doctor-scenario-preflight.json +45 -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/reports-create-redacted.json +1 -1
  10. package/contracts/v1/fixtures/run-timeout-partial.json +15 -4
  11. package/contracts/v1/fixtures/stream-in-progress.ndjson +3 -0
  12. package/contracts/v1/fixtures/stream-submitted-completed.ndjson +1 -1
  13. package/contracts/v1/fixtures/surface-chat-legacy.json +35 -0
  14. package/contracts/v1/fixtures/surface-chat-simplified.json +37 -0
  15. package/contracts/v1/fixtures/surface-sidebar-false-positive.json +29 -0
  16. package/contracts/v1/fixtures/surface-work-advanced.json +45 -0
  17. package/contracts/v1/fixtures/surface-work-basic.json +38 -0
  18. package/contracts/v1/fixtures/workflow-ask-success.json +8 -0
  19. package/contracts/v1/manifest.json +32 -1
  20. package/contracts/v1/parity-suite.json +121 -1
  21. package/contracts/v1/schemas/backend-request.schema.json +15 -0
  22. package/contracts/v1/schemas/manifest.schema.json +6 -3
  23. package/contracts/v1/schemas/surface-profile.schema.json +166 -0
  24. package/dist/codex-chatgpt-control-backend.mjs +3198 -340
  25. package/dist/codex-chatgpt-control.bundle.mjs +4454 -1555
  26. package/dist/src/backend/client.d.ts +26 -1
  27. package/dist/src/backend/client.js +23 -1
  28. package/dist/src/backend/protocol.d.ts +1 -1
  29. package/dist/src/backend/protocol.js +11 -0
  30. package/dist/src/backend/session.js +22 -0
  31. package/dist/src/browser/attach.d.ts +1 -0
  32. package/dist/src/browser/attach.js +3 -3
  33. package/dist/src/browser/clipboard.d.ts +11 -0
  34. package/dist/src/browser/clipboard.js +34 -7
  35. package/dist/src/browser/page-state.js +17 -4
  36. package/dist/src/client.d.ts +32 -1
  37. package/dist/src/client.js +72 -16
  38. package/dist/src/commands/artifacts.js +1 -7
  39. package/dist/src/commands/configuration.d.ts +15 -0
  40. package/dist/src/commands/configuration.js +674 -0
  41. package/dist/src/commands/context.js +3 -1
  42. package/dist/src/commands/conversation.d.ts +15 -0
  43. package/dist/src/commands/conversation.js +44 -0
  44. package/dist/src/commands/deadline.d.ts +8 -0
  45. package/dist/src/commands/deadline.js +14 -0
  46. package/dist/src/commands/doctor.js +47 -3
  47. package/dist/src/commands/experience.d.ts +12 -0
  48. package/dist/src/commands/experience.js +288 -0
  49. package/dist/src/commands/files.js +61 -13
  50. package/dist/src/commands/messages.d.ts +2 -1
  51. package/dist/src/commands/messages.js +349 -87
  52. package/dist/src/commands/modes.d.ts +4 -1
  53. package/dist/src/commands/modes.js +243 -44
  54. package/dist/src/commands/probes.d.ts +15 -0
  55. package/dist/src/commands/probes.js +64 -0
  56. package/dist/src/commands/registry.js +102 -8
  57. package/dist/src/commands/reports.js +1 -1
  58. package/dist/src/commands/response-actions.js +1 -7
  59. package/dist/src/commands/sequence.js +24 -1
  60. package/dist/src/commands/session.d.ts +2 -0
  61. package/dist/src/commands/session.js +50 -1
  62. package/dist/src/commands/threads.js +3 -31
  63. package/dist/src/commands/work.d.ts +6 -0
  64. package/dist/src/commands/work.js +338 -0
  65. package/dist/src/dom/generation-state.d.ts +7 -0
  66. package/dist/src/dom/generation-state.js +7 -1
  67. package/dist/src/dom/label-match.d.ts +3 -0
  68. package/dist/src/dom/label-match.js +25 -0
  69. package/dist/src/dom/locale/am.d.ts +8 -1
  70. package/dist/src/dom/locale/am.js +8 -1
  71. package/dist/src/dom/locale/ar.d.ts +9 -1
  72. package/dist/src/dom/locale/ar.js +9 -1
  73. package/dist/src/dom/locale/bg.d.ts +9 -1
  74. package/dist/src/dom/locale/bg.js +9 -1
  75. package/dist/src/dom/locale/bn.d.ts +9 -1
  76. package/dist/src/dom/locale/bn.js +9 -1
  77. package/dist/src/dom/locale/bs.d.ts +8 -1
  78. package/dist/src/dom/locale/bs.js +8 -1
  79. package/dist/src/dom/locale/ca.d.ts +8 -1
  80. package/dist/src/dom/locale/ca.js +8 -1
  81. package/dist/src/dom/locale/cs.d.ts +8 -1
  82. package/dist/src/dom/locale/cs.js +8 -1
  83. package/dist/src/dom/locale/da.d.ts +7 -1
  84. package/dist/src/dom/locale/da.js +7 -1
  85. package/dist/src/dom/locale/de.d.ts +10 -3
  86. package/dist/src/dom/locale/de.js +10 -3
  87. package/dist/src/dom/locale/el.d.ts +8 -1
  88. package/dist/src/dom/locale/el.js +8 -1
  89. package/dist/src/dom/locale/en.d.ts +40 -1
  90. package/dist/src/dom/locale/en.js +42 -1
  91. package/dist/src/dom/locale/es-419.d.ts +8 -1
  92. package/dist/src/dom/locale/es-419.js +8 -1
  93. package/dist/src/dom/locale/es-ES.d.ts +8 -1
  94. package/dist/src/dom/locale/es-ES.js +8 -1
  95. package/dist/src/dom/locale/et.d.ts +8 -1
  96. package/dist/src/dom/locale/et.js +8 -1
  97. package/dist/src/dom/locale/fa.d.ts +9 -1
  98. package/dist/src/dom/locale/fa.js +9 -1
  99. package/dist/src/dom/locale/fi.d.ts +8 -1
  100. package/dist/src/dom/locale/fi.js +8 -1
  101. package/dist/src/dom/locale/fr-CA.d.ts +8 -1
  102. package/dist/src/dom/locale/fr-CA.js +8 -1
  103. package/dist/src/dom/locale/fr-FR.d.ts +8 -3
  104. package/dist/src/dom/locale/fr-FR.js +8 -3
  105. package/dist/src/dom/locale/gu.d.ts +8 -1
  106. package/dist/src/dom/locale/gu.js +8 -1
  107. package/dist/src/dom/locale/hi.d.ts +8 -1
  108. package/dist/src/dom/locale/hi.js +8 -1
  109. package/dist/src/dom/locale/hr.d.ts +7 -1
  110. package/dist/src/dom/locale/hr.js +7 -1
  111. package/dist/src/dom/locale/hu.d.ts +8 -1
  112. package/dist/src/dom/locale/hu.js +8 -1
  113. package/dist/src/dom/locale/hy.d.ts +9 -1
  114. package/dist/src/dom/locale/hy.js +9 -1
  115. package/dist/src/dom/locale/id.d.ts +8 -1
  116. package/dist/src/dom/locale/id.js +8 -1
  117. package/dist/src/dom/locale/index.d.ts +20 -1
  118. package/dist/src/dom/locale/index.js +102 -0
  119. package/dist/src/dom/locale/is.d.ts +8 -1
  120. package/dist/src/dom/locale/is.js +8 -1
  121. package/dist/src/dom/locale/it.d.ts +8 -1
  122. package/dist/src/dom/locale/it.js +8 -1
  123. package/dist/src/dom/locale/ja.d.ts +8 -1
  124. package/dist/src/dom/locale/ja.js +8 -1
  125. package/dist/src/dom/locale/ka.d.ts +8 -1
  126. package/dist/src/dom/locale/ka.js +8 -1
  127. package/dist/src/dom/locale/kk.d.ts +8 -1
  128. package/dist/src/dom/locale/kk.js +8 -1
  129. package/dist/src/dom/locale/kn.d.ts +9 -1
  130. package/dist/src/dom/locale/kn.js +9 -1
  131. package/dist/src/dom/locale/ko.d.ts +8 -1
  132. package/dist/src/dom/locale/ko.js +8 -1
  133. package/dist/src/dom/locale/lt.d.ts +9 -1
  134. package/dist/src/dom/locale/lt.js +9 -1
  135. package/dist/src/dom/locale/lv.d.ts +8 -1
  136. package/dist/src/dom/locale/lv.js +8 -1
  137. package/dist/src/dom/locale/mk.d.ts +7 -1
  138. package/dist/src/dom/locale/mk.js +7 -1
  139. package/dist/src/dom/locale/ml.d.ts +9 -1
  140. package/dist/src/dom/locale/ml.js +9 -1
  141. package/dist/src/dom/locale/mn.d.ts +9 -1
  142. package/dist/src/dom/locale/mn.js +9 -1
  143. package/dist/src/dom/locale/mr.d.ts +9 -1
  144. package/dist/src/dom/locale/mr.js +9 -1
  145. package/dist/src/dom/locale/ms.d.ts +8 -1
  146. package/dist/src/dom/locale/ms.js +8 -1
  147. package/dist/src/dom/locale/my.d.ts +8 -1
  148. package/dist/src/dom/locale/my.js +8 -1
  149. package/dist/src/dom/locale/nb.d.ts +8 -1
  150. package/dist/src/dom/locale/nb.js +8 -1
  151. package/dist/src/dom/locale/nl.d.ts +8 -1
  152. package/dist/src/dom/locale/nl.js +8 -1
  153. package/dist/src/dom/locale/pa.d.ts +9 -1
  154. package/dist/src/dom/locale/pa.js +9 -1
  155. package/dist/src/dom/locale/pl.d.ts +8 -1
  156. package/dist/src/dom/locale/pl.js +8 -1
  157. package/dist/src/dom/locale/pt-BR.d.ts +8 -1
  158. package/dist/src/dom/locale/pt-BR.js +8 -1
  159. package/dist/src/dom/locale/pt-PT.d.ts +8 -1
  160. package/dist/src/dom/locale/pt-PT.js +8 -1
  161. package/dist/src/dom/locale/ro.d.ts +7 -1
  162. package/dist/src/dom/locale/ro.js +7 -1
  163. package/dist/src/dom/locale/ru.d.ts +5 -1
  164. package/dist/src/dom/locale/ru.js +5 -1
  165. package/dist/src/dom/locale/sk.d.ts +8 -1
  166. package/dist/src/dom/locale/sk.js +8 -1
  167. package/dist/src/dom/locale/sl.d.ts +8 -1
  168. package/dist/src/dom/locale/sl.js +8 -1
  169. package/dist/src/dom/locale/so.d.ts +8 -1
  170. package/dist/src/dom/locale/so.js +8 -1
  171. package/dist/src/dom/locale/sq.d.ts +8 -1
  172. package/dist/src/dom/locale/sq.js +8 -1
  173. package/dist/src/dom/locale/sr.d.ts +5 -1
  174. package/dist/src/dom/locale/sr.js +5 -1
  175. package/dist/src/dom/locale/sv.d.ts +8 -1
  176. package/dist/src/dom/locale/sv.js +8 -1
  177. package/dist/src/dom/locale/sw.d.ts +8 -1
  178. package/dist/src/dom/locale/sw.js +8 -1
  179. package/dist/src/dom/locale/ta.d.ts +9 -1
  180. package/dist/src/dom/locale/ta.js +9 -1
  181. package/dist/src/dom/locale/te.d.ts +9 -1
  182. package/dist/src/dom/locale/te.js +9 -1
  183. package/dist/src/dom/locale/th.d.ts +8 -1
  184. package/dist/src/dom/locale/th.js +8 -1
  185. package/dist/src/dom/locale/tl.d.ts +4 -5
  186. package/dist/src/dom/locale/tl.js +4 -5
  187. package/dist/src/dom/locale/tr.d.ts +8 -1
  188. package/dist/src/dom/locale/tr.js +8 -1
  189. package/dist/src/dom/locale/types.d.ts +25 -1
  190. package/dist/src/dom/locale/uk.d.ts +8 -1
  191. package/dist/src/dom/locale/uk.js +8 -1
  192. package/dist/src/dom/locale/ur.d.ts +8 -1
  193. package/dist/src/dom/locale/ur.js +8 -1
  194. package/dist/src/dom/locale/vi.d.ts +8 -1
  195. package/dist/src/dom/locale/vi.js +8 -1
  196. package/dist/src/dom/locale/zh-HK.d.ts +9 -1
  197. package/dist/src/dom/locale/zh-HK.js +10 -2
  198. package/dist/src/dom/locale/zh-Hans.d.ts +9 -1
  199. package/dist/src/dom/locale/zh-Hans.js +9 -1
  200. package/dist/src/dom/locale/zh-TW.d.ts +9 -1
  201. package/dist/src/dom/locale/zh-TW.js +9 -1
  202. package/dist/src/dom/menus.js +13 -2
  203. package/dist/src/dom/selectors.js +6 -1
  204. package/dist/src/dom/wait-snapshot.d.ts +43 -0
  205. package/dist/src/dom/wait-snapshot.js +127 -0
  206. package/dist/src/index.d.ts +3 -0
  207. package/dist/src/index.js +3 -0
  208. package/dist/src/runner/responses.d.ts +3 -1
  209. package/dist/src/runner/responses.js +16 -1
  210. package/dist/src/runner/result.js +129 -4
  211. package/dist/src/runner/stream.d.ts +1 -1
  212. package/dist/src/runner/stream.js +6 -0
  213. package/dist/src/runner/types.d.ts +35 -1
  214. package/dist/src/safety/risk.d.ts +11 -0
  215. package/dist/src/safety/risk.js +11 -0
  216. package/dist/src/scripts/apply-intelligence-locale-captures.d.ts +6 -0
  217. package/dist/src/scripts/apply-intelligence-locale-captures.js +147 -15
  218. package/dist/src/scripts/capture-intelligence-locales.d.ts +3 -0
  219. package/dist/src/scripts/capture-intelligence-locales.js +372 -3
  220. package/dist/src/scripts/capture-surface-profile.d.ts +23 -0
  221. package/dist/src/scripts/capture-surface-profile.js +304 -0
  222. package/dist/src/scripts/live-smoke/scenarios.js +34 -1
  223. package/dist/src/scripts/live-smoke.js +1 -0
  224. package/dist/src/types.d.ts +273 -2
  225. package/package.json +4 -3
  226. package/references/2026-07-16-chat-work-surfaces.md +87 -0
  227. package/references/agents-runner.md +13 -2
  228. package/references/backend-protocol.md +19 -7
  229. package/references/language-coverage.md +40 -3
  230. package/references/localization.md +48 -10
  231. package/references/python-parity.md +16 -4
  232. package/references/responses-adapter.md +12 -1
  233. package/references/troubleshooting.md +49 -6
@@ -11,6 +11,11 @@ const CHATGPT_HOME = "https://chatgpt.com/";
11
11
  const DEFAULT_SWITCH_TIMEOUT_MS = 15_000;
12
12
  const DEFAULT_SETTLE_MS = 1_500;
13
13
  const DEFAULT_LIMIT = 25;
14
+ const DEFAULT_GENERATION_CAPTURE_TIMEOUT_MS = 8_000;
15
+ const DEFAULT_GENERATION_PROMPT = [
16
+ "Localization probe: count upward from 1 to 2000, one number per line.",
17
+ "Do not explain. Keep going until I stop you."
18
+ ].join(" ");
14
19
  class CaptureUsageError extends Error {
15
20
  exitCode;
16
21
  constructor(message, exitCode = 2) {
@@ -38,6 +43,10 @@ const USAGE = [
38
43
  " --locales Comma-separated BCP47 ids to sweep instead of first --limit languages.",
39
44
  " --open-version-submenu Capture GPT-* model version submenu labels. Default: true.",
40
45
  " --no-open-version-submenu Do not open the model-version submenu.",
46
+ " --capture-generation-state Submit one bounded probe per locale to capture localized running/stopped generation labels. Default: false.",
47
+ " --no-capture-generation-state Disable generation-state capture.",
48
+ " --generation-prompt Override the redacted probe prompt used only for generation-state capture.",
49
+ " --generation-timeout-ms Wait for generation controls after submit. Default: 8000.",
41
50
  " --restore Restore the initially selected language after a sweep. Default with --auto-switch.",
42
51
  " --no-restore Leave ChatGPT on the last swept language.",
43
52
  " --settle-ms Wait after language switches and menu opens. Default: 1500.",
@@ -114,6 +123,9 @@ export function parseArgs(argv) {
114
123
  let limit;
115
124
  let locales;
116
125
  let openVersionSubmenu = true;
126
+ let captureGenerationState = false;
127
+ let generationPrompt = DEFAULT_GENERATION_PROMPT;
128
+ let generationCaptureTimeoutMs = DEFAULT_GENERATION_CAPTURE_TIMEOUT_MS;
117
129
  let restore;
118
130
  let settleMs = DEFAULT_SETTLE_MS;
119
131
  let switchTimeoutMs = DEFAULT_SWITCH_TIMEOUT_MS;
@@ -158,6 +170,18 @@ export function parseArgs(argv) {
158
170
  case "--no-open-version-submenu":
159
171
  openVersionSubmenu = false;
160
172
  break;
173
+ case "--capture-generation-state":
174
+ captureGenerationState = true;
175
+ break;
176
+ case "--no-capture-generation-state":
177
+ captureGenerationState = false;
178
+ break;
179
+ case "--generation-prompt":
180
+ generationPrompt = requiredValue(argv, ++index, arg);
181
+ break;
182
+ case "--generation-timeout-ms":
183
+ generationCaptureTimeoutMs = parsePositiveInteger(requiredValue(argv, ++index, arg), arg);
184
+ break;
161
185
  case "--restore":
162
186
  restore = true;
163
187
  break;
@@ -197,6 +221,9 @@ export function parseArgs(argv) {
197
221
  limit,
198
222
  locales,
199
223
  openVersionSubmenu,
224
+ captureGenerationState,
225
+ generationPrompt,
226
+ generationCaptureTimeoutMs,
200
227
  restore: restore ?? autoSwitch,
201
228
  settleMs,
202
229
  switchTimeoutMs,
@@ -260,8 +287,15 @@ async function captureOne(page, language, options) {
260
287
  }
261
288
  }
262
289
  await closeSettingsIfOpen(page);
290
+ await returnToChatSurface(page, options);
263
291
  const picker = await captureIntelligencePicker(page, options);
264
- return {
292
+ const generation = options.captureGenerationState
293
+ ? await captureGenerationStateLabels(page, options)
294
+ : undefined;
295
+ if (generation !== undefined) {
296
+ warnings.push(...generation.warnings);
297
+ }
298
+ const record = {
265
299
  schemaVersion: SCHEMA_VERSION,
266
300
  status: "ok",
267
301
  capturedAt: new Date().toISOString(),
@@ -276,6 +310,12 @@ async function captureOne(page, language, options) {
276
310
  modelVersionLabels: picker.modelVersionLabels,
277
311
  warnings
278
312
  };
313
+ if (generation !== undefined) {
314
+ record.generationStopLabels = generation.stopLabels;
315
+ record.generationStoppedLabels = generation.stoppedLabels;
316
+ record.generationSignals = generation.signals;
317
+ }
318
+ return record;
279
319
  }
280
320
  catch (error) {
281
321
  const proof = await renderedProof(page).catch(() => ({}));
@@ -378,6 +418,18 @@ async function closeSettingsIfOpen(page) {
378
418
  throw new Error("Settings modal did not close.");
379
419
  }
380
420
  }
421
+ async function returnToChatSurface(page, options) {
422
+ const proof = await renderedProof(page).catch(() => ({}));
423
+ if (proof.url?.includes("#settings") !== true)
424
+ return;
425
+ if (page.goto !== undefined) {
426
+ await page.goto(CHATGPT_HOME).catch(() => undefined);
427
+ await wait(options.settleMs);
428
+ return;
429
+ }
430
+ await page.keyboard?.press?.("Escape").catch(() => undefined);
431
+ await wait(options.settleMs);
432
+ }
381
433
  async function closeFloatingMenus(page) {
382
434
  await page.locator?.("body")?.click?.({ position: { x: 12, y: 12 } }).catch(() => undefined);
383
435
  await wait(250);
@@ -544,6 +596,323 @@ async function openVersionSubmenu(page) {
544
596
  return;
545
597
  }
546
598
  }
599
+ async function captureGenerationStateLabels(page, options) {
600
+ const warnings = [];
601
+ const before = await readGenerationUiSnapshot(page).catch(() => ({ controls: [], shortLatestAssistantTexts: [] }));
602
+ let submitted = false;
603
+ let stopped = false;
604
+ let active = before;
605
+ let stopLabels = [];
606
+ try {
607
+ if (snapshotLooksActive(before)) {
608
+ warnings.push("Generation controls were already visible before the probe; capturing existing controls without submitting another prompt.");
609
+ }
610
+ else {
611
+ submitted = await submitGenerationProbePrompt(page, options.generationPrompt);
612
+ }
613
+ active = await waitForGenerationUiSnapshot(page, before, options.generationCaptureTimeoutMs).catch(error => {
614
+ warnings.push(`Generation control capture timed out: ${error instanceof Error ? error.message : String(error)}`);
615
+ return before;
616
+ });
617
+ stopLabels = generationStopLabels(before, active);
618
+ if (stopLabels.length === 0) {
619
+ warnings.push("No generation stop labels were observed; leaving stopControl unchanged for this locale.");
620
+ }
621
+ }
622
+ catch (error) {
623
+ warnings.push(`Generation probe failed: ${error instanceof Error ? error.message : String(error)}`);
624
+ }
625
+ finally {
626
+ if (submitted || stopLabels.length > 0 || snapshotLooksActive(active)) {
627
+ stopped = await stopGenerationIfVisible(page, stopLabels).catch(error => {
628
+ warnings.push(`Unable to stop generation after probe: ${error instanceof Error ? error.message : String(error)}`);
629
+ return false;
630
+ });
631
+ await wait(options.settleMs);
632
+ }
633
+ }
634
+ const afterStop = await readGenerationUiSnapshot(page).catch(() => ({ controls: [], shortLatestAssistantTexts: [] }));
635
+ const stoppedLabels = stopped ? generationStoppedLabels(before, active, afterStop) : [];
636
+ if (submitted && !stopped) {
637
+ warnings.push("Generation probe was submitted but no stop action was confirmed.");
638
+ }
639
+ return {
640
+ stopLabels,
641
+ stoppedLabels,
642
+ signals: dedupeStrings([
643
+ ...stopLabels,
644
+ ...stoppedLabels,
645
+ ...active.controls.map(control => control.label)
646
+ ]).slice(0, 20),
647
+ warnings,
648
+ submitted,
649
+ stopped
650
+ };
651
+ }
652
+ async function submitGenerationProbePrompt(page, prompt) {
653
+ await closeFloatingMenus(page);
654
+ const textbox = page.locator?.("textarea, [contenteditable='true']")?.last?.()
655
+ ?? page.getByRole?.("textbox")?.last?.();
656
+ if (textbox?.click === undefined || textbox.fill === undefined) {
657
+ throw new Error("Composer textbox was not available for generation probe.");
658
+ }
659
+ await textbox.click();
660
+ await textbox.fill(prompt);
661
+ if (page.keyboard?.press !== undefined) {
662
+ await page.keyboard.press("Enter");
663
+ return true;
664
+ }
665
+ const clicked = await clickSubmitControlByDom(page);
666
+ if (!clicked) {
667
+ throw new Error("No structural submit control was available for generation probe.");
668
+ }
669
+ return true;
670
+ }
671
+ async function clickSubmitControlByDom(page) {
672
+ const structuralSelectors = [
673
+ "form button[data-testid='send-button']",
674
+ "form button#composer-submit-button",
675
+ "button[data-testid='send-button']",
676
+ "button#composer-submit-button"
677
+ ];
678
+ for (const selector of structuralSelectors) {
679
+ const button = page.locator?.(selector)?.last?.();
680
+ if (button?.click === undefined)
681
+ continue;
682
+ const clicked = await button.click().then(() => true, () => false);
683
+ if (clicked)
684
+ return true;
685
+ }
686
+ if (typeof page.evaluate !== "function")
687
+ return false;
688
+ return page.evaluate(() => {
689
+ const isButtonElement = (element) => element.tagName.toLowerCase() === "button";
690
+ const visible = (element) => {
691
+ const rect = element.getBoundingClientRect();
692
+ const style = window.getComputedStyle(element);
693
+ return rect.width > 0
694
+ && rect.height > 0
695
+ && style.display !== "none"
696
+ && style.visibility !== "hidden"
697
+ && style.opacity !== "0";
698
+ };
699
+ const buttons = Array.from(document.querySelectorAll("form button, button"))
700
+ .filter((button) => {
701
+ if (!isButtonElement(button))
702
+ return false;
703
+ if (button.disabled || button.getAttribute("aria-disabled") === "true")
704
+ return false;
705
+ if (!visible(button))
706
+ return false;
707
+ const text = [
708
+ button.textContent,
709
+ button.getAttribute("aria-label"),
710
+ button.getAttribute("title"),
711
+ button.getAttribute("data-testid")
712
+ ].filter(Boolean).join(" ");
713
+ return /send|submit|composer-submit|arrow-up/i.test(text);
714
+ });
715
+ let button = buttons.at(-1);
716
+ if (button === undefined) {
717
+ const structural = Array.from(document.querySelectorAll("form button"))
718
+ .filter((candidate) => {
719
+ if (!isButtonElement(candidate))
720
+ return false;
721
+ if (candidate.disabled || candidate.getAttribute("aria-disabled") === "true")
722
+ return false;
723
+ if (!visible(candidate))
724
+ return false;
725
+ const text = [
726
+ candidate.textContent,
727
+ candidate.getAttribute("aria-label"),
728
+ candidate.getAttribute("title"),
729
+ candidate.getAttribute("data-testid"),
730
+ candidate.id
731
+ ].filter(Boolean).join(" ");
732
+ return !/composer-plus|plus|attach|file|microphone|mic|dictat|voice|audio|intelligence|model|tool/i.test(text);
733
+ })
734
+ .sort((left, right) => {
735
+ const leftRect = left.getBoundingClientRect();
736
+ const rightRect = right.getBoundingClientRect();
737
+ return (rightRect.bottom + rightRect.right / 10) - (leftRect.bottom + leftRect.right / 10);
738
+ });
739
+ button = structural[0];
740
+ }
741
+ if (button === undefined)
742
+ return false;
743
+ button.click();
744
+ return true;
745
+ }).catch(() => false);
746
+ }
747
+ async function waitForGenerationUiSnapshot(page, before, timeoutMs) {
748
+ const deadline = Date.now() + timeoutMs;
749
+ for (;;) {
750
+ const current = await readGenerationUiSnapshot(page);
751
+ if (generationStopLabels(before, current).length > 0 || snapshotLooksActive(current)) {
752
+ return current;
753
+ }
754
+ if (Date.now() >= deadline) {
755
+ throw new Error("Timed out waiting for generation controls.");
756
+ }
757
+ await wait(500);
758
+ }
759
+ }
760
+ async function readGenerationUiSnapshot(page) {
761
+ if (typeof page.evaluate !== "function") {
762
+ return { controls: [], shortLatestAssistantTexts: [] };
763
+ }
764
+ return page.evaluate(() => {
765
+ const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim();
766
+ const visible = (element) => {
767
+ if (typeof element.getBoundingClientRect !== "function")
768
+ return false;
769
+ const rect = element.getBoundingClientRect();
770
+ const style = window.getComputedStyle(element);
771
+ return rect.width > 0
772
+ && rect.height > 0
773
+ && style.display !== "none"
774
+ && style.visibility !== "hidden"
775
+ && style.opacity !== "0"
776
+ && element.getAttribute("aria-hidden") !== "true";
777
+ };
778
+ const relevantSurface = (element) => element.closest("main, form") !== null
779
+ && element.closest("nav, aside") === null;
780
+ const controls = Array.from(document.querySelectorAll("main button, main [role='button'], form button, form [role='button']"))
781
+ .filter(visible)
782
+ .filter(relevantSurface)
783
+ .map(element => {
784
+ const html = element;
785
+ const text = normalize(html.innerText || html.textContent);
786
+ const ariaLabel = normalize(html.getAttribute("aria-label"));
787
+ const title = normalize(html.getAttribute("title"));
788
+ const testId = normalize(html.getAttribute("data-testid"));
789
+ const label = ariaLabel || title || text || testId;
790
+ return {
791
+ label,
792
+ text: text || undefined,
793
+ ariaLabel: ariaLabel || undefined,
794
+ title: title || undefined,
795
+ testId: testId || undefined,
796
+ role: html.getAttribute("role") ?? undefined
797
+ };
798
+ })
799
+ .filter(control => control.label.length > 0);
800
+ const turns = Array.from(document.querySelectorAll("main [data-testid^='conversation-turn']"));
801
+ const latestAssistant = turns.reverse().find(turn => turn.querySelector("[data-message-author-role='assistant']") !== null);
802
+ const shortLatestAssistantTexts = latestAssistant === undefined
803
+ ? []
804
+ : Array.from(latestAssistant.querySelectorAll("[data-message-author-role='assistant'] *"))
805
+ .map(element => normalize(element.innerText || element.textContent))
806
+ .filter(text => text.length > 0 && text.length <= 80);
807
+ return { controls, shortLatestAssistantTexts };
808
+ }).catch(() => ({ controls: [], shortLatestAssistantTexts: [] }));
809
+ }
810
+ function generationStopLabels(before, active) {
811
+ const beforeLabels = new Set(before.controls.map(control => normalizedControlKey(control.label)));
812
+ const candidates = active.controls
813
+ .filter(control => !beforeLabels.has(normalizedControlKey(control.label)) || looksLikeStopControl(control))
814
+ .map(control => control.label)
815
+ .filter(isUsefulGenerationLabel);
816
+ return dedupeStrings(candidates);
817
+ }
818
+ function generationStoppedLabels(before, active, afterStop) {
819
+ const previous = new Set([...before.shortLatestAssistantTexts, ...active.shortLatestAssistantTexts].map(normalizedControlKey));
820
+ const candidates = afterStop.shortLatestAssistantTexts
821
+ .filter(text => !previous.has(normalizedControlKey(text)))
822
+ .filter(isUsefulStoppedText);
823
+ return dedupeStrings(candidates);
824
+ }
825
+ function snapshotLooksActive(snapshot) {
826
+ return snapshot.controls.some(looksLikeStopControl);
827
+ }
828
+ function looksLikeStopControl(control) {
829
+ return /stop|cancel|abort|interromp|unterbrech|gestoppt|arr[eê]t|detener|parar|interrumpir/i.test([
830
+ control.label,
831
+ control.text,
832
+ control.ariaLabel,
833
+ control.title,
834
+ control.testId
835
+ ].filter(Boolean).join(" "));
836
+ }
837
+ function isUsefulGenerationLabel(label) {
838
+ const normalized = normalizedControlKey(label);
839
+ if (normalized.length < 2 || normalized.length > 80)
840
+ return false;
841
+ if (/^(send|send prompt|voice|dictate|start dictation|attach|add files|new chat|copy response|more actions|pro|instant|thinking|extended thinking)$/i.test(normalized))
842
+ return false;
843
+ return true;
844
+ }
845
+ function isUsefulStoppedText(text) {
846
+ const normalized = normalizedControlKey(text);
847
+ if (normalized.length < 2 || normalized.length > 80)
848
+ return false;
849
+ if (/^[\d\s.,:;!?()[\]-]+$/.test(normalized))
850
+ return false;
851
+ if (/^localization probe/i.test(normalized))
852
+ return false;
853
+ return true;
854
+ }
855
+ async function stopGenerationIfVisible(page, labels) {
856
+ for (const label of labels) {
857
+ const roleButton = page.getByRole?.("button", { name: label, exact: true })?.last?.();
858
+ if (roleButton?.click !== undefined) {
859
+ await roleButton.click().catch(() => undefined);
860
+ await wait(500);
861
+ return true;
862
+ }
863
+ }
864
+ if (typeof page.evaluate !== "function")
865
+ return false;
866
+ return page.evaluate((wantedLabels) => {
867
+ const normalize = (value) => (value ?? "").replace(/\s+/g, " ").trim().toLowerCase();
868
+ const wanted = new Set(wantedLabels.map(label => label.toLowerCase()));
869
+ const relevantSurface = (element) => element.closest("main, form") !== null
870
+ && element.closest("nav, aside") === null;
871
+ const visible = (element) => {
872
+ const rect = element.getBoundingClientRect();
873
+ const style = window.getComputedStyle(element);
874
+ return rect.width > 0
875
+ && rect.height > 0
876
+ && style.display !== "none"
877
+ && style.visibility !== "hidden"
878
+ && style.opacity !== "0";
879
+ };
880
+ const buttons = Array.from(document.querySelectorAll("main button, main [role='button'], form button, form [role='button']"))
881
+ .filter((button) => typeof button.getBoundingClientRect === "function" && visible(button))
882
+ .filter(relevantSurface);
883
+ const match = buttons.find(button => {
884
+ const label = [
885
+ button.getAttribute("aria-label"),
886
+ button.getAttribute("title"),
887
+ button.innerText,
888
+ button.textContent,
889
+ button.getAttribute("data-testid")
890
+ ].map(normalize).filter(Boolean).join(" ");
891
+ return wanted.has(label)
892
+ || /stop|cancel|abort|interromp|unterbrech|gestoppt|arr[eê]t|detener|parar|interrumpir/i.test(label);
893
+ });
894
+ if (match === undefined)
895
+ return false;
896
+ match.click();
897
+ return true;
898
+ }, [...labels]).catch(() => false);
899
+ }
900
+ function normalizedControlKey(label) {
901
+ return label.replace(/\s+/g, " ").trim().toLowerCase();
902
+ }
903
+ function dedupeStrings(values) {
904
+ const seen = new Set();
905
+ const result = [];
906
+ for (const value of values) {
907
+ const trimmed = value.replace(/\s+/g, " ").trim();
908
+ const key = trimmed.toLowerCase();
909
+ if (trimmed.length === 0 || seen.has(key))
910
+ continue;
911
+ seen.add(key);
912
+ result.push(trimmed);
913
+ }
914
+ return result;
915
+ }
547
916
  async function renderedProof(page) {
548
917
  return page.evaluate?.(() => ({
549
918
  htmlLang: document.documentElement.lang,
@@ -588,7 +957,7 @@ async function appendRecord(path, record) {
588
957
  }
589
958
  function printCaptureRecord(record, out) {
590
959
  if (record.status === "ok") {
591
- console.log(`captured ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} intelligence=${record.intelligenceLabels?.length ?? 0} versions=${record.modelVersionLabels?.length ?? 0} out=${out}`);
960
+ console.log(`captured ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} intelligence=${record.intelligenceLabels?.length ?? 0} versions=${record.modelVersionLabels?.length ?? 0} generationStop=${record.generationStopLabels?.length ?? 0} generationStopped=${record.generationStoppedLabels?.length ?? 0} out=${out}`);
592
961
  }
593
962
  else {
594
963
  console.log(`blocked ${record.requestedLocale} htmlLang=${record.htmlLang ?? "unknown"} code=${record.blocker?.code ?? "unknown"} out=${out}`);
@@ -663,7 +1032,7 @@ function wait(ms) {
663
1032
  function escapeRegExp(value) {
664
1033
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
665
1034
  }
666
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
1035
+ if (typeof process !== "undefined" && process.argv[1] === fileURLToPath(import.meta.url)) {
667
1036
  const exitCode = await main();
668
1037
  process.exitCode = exitCode;
669
1038
  }
@@ -0,0 +1,23 @@
1
+ import { detectExperienceFromSnapshot, readSurfaceSnapshot } from "../commands/experience.js";
2
+ import type { BrowserLike, ConfigurationInspectionData, ExistingTabPolicy, SurfaceProfileFixture, SurfaceProfileSupportState } from "../types.js";
3
+ type CaptureRuntime = {
4
+ agent?: unknown;
5
+ browser?: BrowserLike;
6
+ };
7
+ type CaptureOptions = {
8
+ id: string;
9
+ out: string;
10
+ locale?: string;
11
+ region: string;
12
+ accountScope: string;
13
+ planScope: string;
14
+ workspaceScope: string;
15
+ supportState: SurfaceProfileSupportState;
16
+ provenance: string;
17
+ tabId?: string;
18
+ ifMissing: NonNullable<ExistingTabPolicy["ifMissing"]>;
19
+ };
20
+ export declare function main(argv?: string[], runtime?: CaptureRuntime): Promise<number>;
21
+ export declare function buildSurfaceProfileDraft(options: Pick<CaptureOptions, "id" | "region" | "accountScope" | "planScope" | "workspaceScope" | "supportState" | "provenance">, locale: string, snapshot: Awaited<ReturnType<typeof readSurfaceSnapshot>>, detected: ReturnType<typeof detectExperienceFromSnapshot>, inspection: ConfigurationInspectionData, observedAt?: string): SurfaceProfileFixture;
22
+ export declare function parseArgs(argv: readonly string[]): CaptureOptions;
23
+ export {};