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
@@ -5,9 +5,27 @@
5
5
  *
6
6
  * Some non-Intelligence surfaces may still fall back to English + `selector_drift`.
7
7
  *
8
- * Intelligence picker labels updated 2026-06-10 and stop-control labels updated 2026-06-15 from visible ChatGPT Pro sessions.
8
+ * Intelligence picker labels updated 2026-06-10, stop-control labels updated 2026-06-15, Chat/Work surface labels updated 2026-07-17, and Power/Advanced selector labels updated 2026-08-08 from visible ChatGPT sessions.
9
9
  */
10
10
  export const zhTW = {
11
+ configurationAxes: {
12
+ power: ["能力"],
13
+ model: ["模型"],
14
+ effort: ["推理強度"],
15
+ speed: ["速度"],
16
+ advanced: ["進階"],
17
+ },
18
+ configurationOptions: {
19
+ instant: ["即時"],
20
+ light: ["快思"],
21
+ medium: ["中"],
22
+ high: ["高"],
23
+ extraHigh: ["極高", "超高"],
24
+ max: ["最高"],
25
+ ultra: ["極致"],
26
+ standard: ["標準"],
27
+ fast: ["快速"],
28
+ },
11
29
  composerTextbox: ["與 ChatGPT 對話"],
12
30
  sendButton: ["傳送提示詞"],
13
31
  searchChatsButton: ["搜尋對話"],
@@ -17,15 +35,19 @@ export const zhTW = {
17
35
  addFilesOpenerCandidates: ["新增檔案等更多功能"],
18
36
  addPhotosFilesMenuItem: ["新增照片和檔案"],
19
37
  copyResponse: ["複製回應"],
20
- modeLabels: ["即時", "中等", "高", "超高", "專業"],
38
+ modeLabels: ["即時", "中等", "高", "超高", "專業", "中"],
21
39
  modeOptions: {
22
40
  instant: ["即時"],
23
- medium: ["中等"],
41
+ medium: ["中等", "中"],
24
42
  high: ["高"],
25
43
  extraHigh: ["超高"],
26
44
  pro: ["專業"],
27
45
  },
28
46
  modeOpenerExtra: ["設定"],
47
+ experienceOptions: {
48
+ chat: ["對話"],
49
+ work: ["工作"],
50
+ },
29
51
  tools: {
30
52
  web_search: ["網頁搜尋"],
31
53
  deep_research: ["深入研究"],
@@ -10,6 +10,28 @@ export function extractMenuItemsFromText(text) {
10
10
  export async function enumerateVisibleMenuItems(page) {
11
11
  if (typeof page.evaluate === "function") {
12
12
  const labels = await page.evaluate(() => {
13
+ const visible = (element) => {
14
+ const html = element;
15
+ const rect = html.getBoundingClientRect?.();
16
+ if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
17
+ return false;
18
+ let current = element;
19
+ while (current !== null) {
20
+ if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
21
+ return false;
22
+ }
23
+ const style = typeof window !== "undefined"
24
+ ? window.getComputedStyle?.(current)
25
+ : undefined;
26
+ if (style?.display === "none"
27
+ || style?.visibility === "hidden"
28
+ || style?.opacity === "0") {
29
+ return false;
30
+ }
31
+ current = current.parentElement ?? null;
32
+ }
33
+ return true;
34
+ };
13
35
  const toItem = (node) => {
14
36
  const element = node;
15
37
  const label = (element.innerText ?? element.textContent ?? "").replace(/\s+/g, " ").trim();
@@ -37,13 +59,14 @@ export async function enumerateVisibleMenuItems(page) {
37
59
  item.ariaLabel = ariaLabel;
38
60
  return item;
39
61
  };
40
- const allRoleNodes = Array.from(document.querySelectorAll("[role='menuitem'], [role='menuitemradio'], [role='option']"));
62
+ const allRoleNodes = Array.from(document.querySelectorAll("[role='menuitem'], [role='menuitemradio'], [role='option']"))
63
+ .filter(visible);
41
64
  // Scope to open menu containers when any exist, so stray role items elsewhere on
42
65
  // the page (sidebar rows, decorative listboxes) cannot contaminate menu matching.
43
66
  // An empty scoped set falls back to the unscoped list: real menus keep their items
44
67
  // inside the container, so an empty intersection means the container heuristic failed.
45
68
  const containers = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]"))
46
- .filter(container => typeof container.contains === "function");
69
+ .filter(container => visible(container) && typeof container.contains === "function");
47
70
  const scopedRoleNodes = containers.length > 0
48
71
  ? allRoleNodes.filter(node => containers.some(container => container.contains(node)))
49
72
  : allRoleNodes;
@@ -54,6 +77,7 @@ export async function enumerateVisibleMenuItems(page) {
54
77
  return { items: roleItems, labels: [], split: false };
55
78
  }
56
79
  const menus = Array.from(document.querySelectorAll("[role='menu'], [role='listbox'], [data-radix-popper-content-wrapper]"))
80
+ .filter(visible)
57
81
  .map(node => node.innerText ?? node.textContent ?? "")
58
82
  .filter(Boolean);
59
83
  return { items: [], labels: menus, split: true };
@@ -10,6 +10,7 @@ export declare const cssSelectors: {
10
10
  };
11
11
  export declare function composerTextbox(page: PageLike): LocatorLike;
12
12
  export declare function sendButton(page: PageLike): LocatorLike;
13
+ export declare function stopGenerationButton(page: PageLike): LocatorLike;
13
14
  export declare function searchChatsButton(page: PageLike): LocatorLike;
14
15
  export declare function searchChatsInput(page: PageLike): LocatorLike;
15
16
  export declare function newChatButton(page: PageLike): LocatorLike;
@@ -1,4 +1,4 @@
1
- import { anyLabelPattern, localeLabels } from "./locale-labels.js";
1
+ import { anyLabelPattern, escapeRegExp, localeLabels } from "./locale-labels.js";
2
2
  // Language-sensitive label tokens are sourced from the locale registry; the structural
3
3
  // clauses (download attributes, file-backend hrefs, blob/data sources) are language-agnostic
4
4
  // and stay literal. For a single English candidate the generated selectors are identical to
@@ -57,6 +57,14 @@ export function sendButton(page) {
57
57
  }
58
58
  return page.getByRole("button", { name: anyLabelPattern(localeLabels.sendButton) });
59
59
  }
60
+ export function stopGenerationButton(page) {
61
+ if (typeof page.getByRole !== "function") {
62
+ return requiredLocator(page, "button[aria-label*='Stop'], button[title*='Stop']");
63
+ }
64
+ return page.getByRole("button", {
65
+ name: new RegExp(`^(?:${localeLabels.stopControl.map(escapeRegExp).join("|")})$`, "i")
66
+ });
67
+ }
60
68
  export function searchChatsButton(page) {
61
69
  if (typeof page.getByRole !== "function") {
62
70
  return requiredLocator(page, "button");
@@ -15,6 +15,7 @@ export type WaitDomSnapshot = {
15
15
  latestAssistantTurnIndex?: number;
16
16
  text: WaitTextMetadata;
17
17
  generation: {
18
+ observed: boolean;
18
19
  active: boolean;
19
20
  stopped: boolean;
20
21
  signals: string[];
@@ -53,35 +53,57 @@ export async function readWaitDomSnapshot(page) {
53
53
  || /^reading (?:the )?images?$/.test(trimmedForTransient);
54
54
  // --- Generation state: mirrors dom/generation-state.ts readAssistantGenerationState ---
55
55
  const isVisible = (element) => {
56
+ if (element.hidden || element.closest("[hidden], [inert], [aria-hidden='true']") !== null) {
57
+ return false;
58
+ }
56
59
  const style = window.getComputedStyle(element);
57
- return style.display !== "none"
58
- && style.visibility !== "hidden"
59
- && style.opacity !== "0"
60
- && element.getAttribute("aria-hidden") !== "true";
60
+ if (style.display === "none"
61
+ || style.visibility === "hidden"
62
+ || style.opacity === "0"
63
+ || style.pointerEvents === "none") {
64
+ return false;
65
+ }
66
+ const rect = element.getBoundingClientRect();
67
+ return rect.width > 0 || rect.height > 0;
68
+ };
69
+ const elementValues = (element) => [
70
+ element.getAttribute("aria-label"),
71
+ element.getAttribute("title"),
72
+ element.innerText,
73
+ element.textContent
74
+ ].map(normalizeLower).filter(Boolean);
75
+ const matchingLabels = (element, phrases) => {
76
+ const values = elementValues(element);
77
+ return phrases.filter(phrase => values.includes(normalizeLower(phrase)));
78
+ };
79
+ const isScopedStopControl = (button) => {
80
+ if (button.matches("[data-testid='stop-button'], [data-testid*='stop' i]"))
81
+ return true;
82
+ if (button.closest("[data-testid*='composer' i], [aria-label*='composer' i]") !== null)
83
+ return true;
84
+ const form = button.closest("form");
85
+ return form?.querySelector("textarea, [contenteditable='true'], [role='textbox']") !== null;
61
86
  };
62
- const visibleButtons = Array.from(document.querySelectorAll("button"))
87
+ const visibleStopButtons = Array.from(document.querySelectorAll("button"))
63
88
  .filter((button) => isVisible(button)
64
89
  && button.disabled !== true
65
- && button.getAttribute("aria-disabled") !== "true");
66
- const buttonTexts = visibleButtons
67
- .map(button => [
68
- button.innerText,
69
- button.textContent,
70
- button.getAttribute("aria-label"),
71
- button.getAttribute("title")
72
- ].map(normalizeLower).filter(Boolean).join(" "))
73
- .filter(Boolean);
74
- const bodyText = normalizeLower(document.body?.innerText);
75
- const haystacks = [bodyText, ...buttonTexts];
76
- const matchingSignals = (phrases) => haystacks.flatMap(text => phrases
77
- .map(phrase => phrase.toLowerCase())
78
- .filter(phrase => text.includes(phrase)));
79
- const activeSignals = matchingSignals(args.stop);
80
- const stoppedSignals = matchingSignals(args.stopped);
90
+ && button.getAttribute("aria-disabled") !== "true"
91
+ && isScopedStopControl(button)
92
+ && matchingLabels(button, args.send).length === 0
93
+ && matchingLabels(button, args.stop).length > 0);
94
+ const activeSignals = [...new Set(visibleStopButtons.flatMap(button => matchingLabels(button, args.stop)))];
95
+ const latestAssistantTurn = latestAssistant?.closest("[data-testid^='conversation-turn']")
96
+ ?? Array.from(document.querySelectorAll("[data-testid^='conversation-turn']")).at(-1);
97
+ const stoppedSignals = latestAssistantTurn === undefined
98
+ ? []
99
+ : [...new Set(Array.from(latestAssistantTurn.querySelectorAll("button, [role='status'], [aria-label], [title], p, span, div"))
100
+ .filter(element => isVisible(element))
101
+ .flatMap(element => matchingLabels(element, args.stopped)))];
81
102
  const generation = {
103
+ observed: true,
82
104
  active: activeSignals.length > 0,
83
105
  stopped: stoppedSignals.length > 0,
84
- signals: [...new Set([...activeSignals, ...stoppedSignals, ...buttonTexts.filter(text => /stop|cancel|stopped|answering|thinking/i.test(text))])].slice(0, 5)
106
+ signals: [...new Set([...activeSignals, ...stoppedSignals])].slice(0, 5)
85
107
  };
86
108
  // --- Response actions: mirrors dom/generation-state.ts latestAssistantTurnHasResponseActions ---
87
109
  const turns = Array.from(document.querySelectorAll("[data-testid^='conversation-turn']"));
@@ -122,6 +144,7 @@ export async function readWaitDomSnapshot(page) {
122
144
  transient: [...localeLabels.transientAssistant],
123
145
  stop: [...localeLabels.stopControl],
124
146
  stopped: [...localeLabels.stoppedAssistant],
147
+ send: [...localeLabels.sendButton],
125
148
  actions: [...localeLabels.responseActions]
126
149
  });
127
150
  }
@@ -1,6 +1,6 @@
1
1
  import type { BlockerKind, CommandContext, CommandResult } from "./types.js";
2
2
  type BlockerDetails = Partial<Omit<NonNullable<CommandResult["blocker"]>, "kind" | "message" | "visibleText">>;
3
- export declare const BROWSER_BRIDGE_UNAVAILABLE_MESSAGE = "Codex cannot access the ChatGPT browser bridge from this backend process. In an ordinary shell this is expected; for a live Codex Chrome run, bootstrap the Chrome plugin runtime with setupBrowserRuntime({ globals: globalThis }) before using globalThis.agent.";
3
+ export declare const BROWSER_BRIDGE_UNAVAILABLE_MESSAGE = "Codex cannot access the ChatGPT browser bridge from this backend process. In an ordinary shell this is expected; for a live Codex Chrome run, assign the Chrome plugin runtime returned by setupBrowserRuntime() to globalThis.agent before using it.";
4
4
  export declare const BROWSER_BRIDGE_REMEDIATION: NonNullable<NonNullable<CommandResult["blocker"]>["remediation"]>;
5
5
  export declare class ChatGPTControlError extends Error {
6
6
  readonly kind: BlockerKind;
@@ -1,4 +1,4 @@
1
- export const BROWSER_BRIDGE_UNAVAILABLE_MESSAGE = "Codex cannot access the ChatGPT browser bridge from this backend process. In an ordinary shell this is expected; for a live Codex Chrome run, bootstrap the Chrome plugin runtime with setupBrowserRuntime({ globals: globalThis }) before using globalThis.agent.";
1
+ export const BROWSER_BRIDGE_UNAVAILABLE_MESSAGE = "Codex cannot access the ChatGPT browser bridge from this backend process. In an ordinary shell this is expected; for a live Codex Chrome run, assign the Chrome plugin runtime returned by setupBrowserRuntime() to globalThis.agent before using it.";
2
2
  export const BROWSER_BRIDGE_REMEDIATION = [
3
3
  {
4
4
  label: "Ordinary shell",
@@ -7,7 +7,7 @@ export const BROWSER_BRIDGE_REMEDIATION = [
7
7
  },
8
8
  {
9
9
  label: "Codex Chrome bootstrap",
10
- instruction: "For a live run, initialize the Chrome plugin runtime in node_repl with setupBrowserRuntime({ globals: globalThis }), then set globalThis.browser = await agent.browsers.get(\"extension\") before calling createChatGPT({ agent: globalThis.agent }).",
10
+ instruction: "For a live run, initialize the Chrome plugin runtime in node_repl with globalThis.agent = await setupBrowserRuntime(), then set globalThis.browser = await agent.browsers.get(\"extension\") before calling createChatGPT({ agent: globalThis.agent }).",
11
11
  userActionRequired: false
12
12
  },
13
13
  {
@@ -1,4 +1,5 @@
1
1
  export type HostPathPlatform = NodeJS.Platform;
2
+ export declare function currentHostPathPlatform(): HostPathPlatform;
2
3
  export declare function isHostAbsolutePath(value: string, platform?: HostPathPlatform): boolean;
3
4
  export declare function resolveForHostPath(value: string, platform?: HostPathPlatform): string;
4
5
  export declare function basenameForHostPath(value: string, platform?: HostPathPlatform): string;
@@ -1,18 +1,22 @@
1
1
  import path from "node:path";
2
- export function isHostAbsolutePath(value, platform = process.platform) {
2
+ import { platform as readHostPlatform } from "node:os";
3
+ export function currentHostPathPlatform() {
4
+ return readHostPlatform();
5
+ }
6
+ export function isHostAbsolutePath(value, platform = currentHostPathPlatform()) {
3
7
  if (value.length === 0)
4
8
  return false;
5
9
  if (platform === "win32")
6
10
  return isFullyQualifiedWindowsPath(value);
7
11
  return path.posix.isAbsolute(value);
8
12
  }
9
- export function resolveForHostPath(value, platform = process.platform) {
13
+ export function resolveForHostPath(value, platform = currentHostPathPlatform()) {
10
14
  if (!isHostAbsolutePath(value, platform)) {
11
15
  throw new Error(`File attachment path must be absolute for the backend host: ${value}`);
12
16
  }
13
17
  return platform === "win32" ? path.win32.resolve(value) : path.posix.resolve(value);
14
18
  }
15
- export function basenameForHostPath(value, platform = process.platform) {
19
+ export function basenameForHostPath(value, platform = currentHostPathPlatform()) {
16
20
  return platform === "win32" ? path.win32.basename(value) : path.posix.basename(value);
17
21
  }
18
22
  function isFullyQualifiedWindowsPath(value) {
@@ -19,6 +19,7 @@ export declare const commandRisk: {
19
19
  readonly "messages.wait": "low";
20
20
  readonly "messages.readLatest": "medium";
21
21
  readonly "messages.status": "medium";
22
+ readonly "messages.stop": "medium";
22
23
  readonly "messages.waitAndRead": "medium";
23
24
  readonly "artifacts.listLatest": "medium";
24
25
  readonly "artifacts.wait": "low";
@@ -18,6 +18,7 @@ export const commandRisk = {
18
18
  "messages.wait": "low",
19
19
  "messages.readLatest": "medium",
20
20
  "messages.status": "medium",
21
+ "messages.stop": "medium",
21
22
  "messages.waitAndRead": "medium",
22
23
  "artifacts.listLatest": "medium",
23
24
  "artifacts.wait": "low",
@@ -1,7 +1,16 @@
1
1
  type IntelligenceModeOptionId = "instant" | "medium" | "high" | "extraHigh" | "pro";
2
+ type ExperienceOptionId = "chat" | "work";
3
+ type ConfigurationAxisId = "model" | "intelligence" | "effort" | "speed" | "power" | "advanced";
4
+ type ConfigurationOptionId = "instant" | "light" | "medium" | "high" | "extraHigh" | "max" | "ultra" | "pro" | "standard" | "fast";
5
+ type SurfaceContribution = {
6
+ workComposerTextbox: string[];
7
+ experienceOptions: Partial<Record<ExperienceOptionId, string[]>>;
8
+ configurationAxes: Partial<Record<ConfigurationAxisId, string[]>>;
9
+ configurationOptions: Partial<Record<ConfigurationOptionId, string[]>>;
10
+ };
2
11
  export declare function main(argv?: string[]): Promise<number>;
3
12
  export declare function mergeCapture(source: string, labels: readonly string[], modeOptions: Partial<Record<IntelligenceModeOptionId, string[]>>, generationLabels?: {
4
13
  stopControl?: readonly string[];
5
14
  stoppedAssistant?: readonly string[];
6
- }): string;
15
+ }, surface?: SurfaceContribution): string;
7
16
  export {};
@@ -4,6 +4,11 @@ import { fileURLToPath } from "node:url";
4
4
  import { readLanguageCoverage } from "./locale-capture/language-coverage.js";
5
5
  const ENGLISH_MODE_LABELS = new Set(["Latest", "Instant", "Thinking", "Extended", "Medium", "High", "Extra High", "Pro"]);
6
6
  const ENGLISH_STOP_CONTROL = new Set(["stop generating", "stop streaming", "stop answering", "cancel"]);
7
+ // These labels are unsafe regardless of language provenance: they are generic
8
+ // controls that can describe Send, dialog dismissal, or unrelated cancellation.
9
+ // Keep this separate from the English-exclusion set so changing the canonical
10
+ // English registry can never make them eligible for a locale stop selector.
11
+ const UNSAFE_GENERIC_STOP_CONTROLS = new Set(["cancel"]);
7
12
  const ENGLISH_STOPPED_ASSISTANT = new Set(["stopped thinking", "stopped answering", "generation stopped"]);
8
13
  const INTELLIGENCE_MODE_OPTION_IDS = ["instant", "medium", "high", "extraHigh", "pro"];
9
14
  const ENGLISH_INTELLIGENCE_MODE_OPTIONS = {
@@ -13,7 +18,37 @@ const ENGLISH_INTELLIGENCE_MODE_OPTIONS = {
13
18
  extraHigh: "Extra High",
14
19
  pro: "Pro",
15
20
  };
16
- const UPDATE_NOTE = " * Intelligence picker labels updated 2026-06-10 and stop-control labels updated 2026-06-15 from visible ChatGPT Pro sessions.";
21
+ const EXPERIENCE_OPTION_IDS = ["chat", "work"];
22
+ const WORK_CONFIGURATION_AXIS_IDS = ["model", "effort", "speed"];
23
+ const CHAT_CONFIGURATION_AXIS_IDS = ["model", "effort"];
24
+ const CONFIGURATION_AXIS_IDS = ["power", "model", "intelligence", "effort", "speed", "advanced"];
25
+ const CONFIGURATION_OPTION_IDS = ["instant", "light", "medium", "high", "extraHigh", "max", "ultra", "pro", "standard", "fast"];
26
+ const CHAT_EFFORT_OPTION_IDS = ["instant", "medium", "high", "extraHigh", "pro"];
27
+ const EFFORT_OPTION_IDS = ["light", "medium", "high", "extraHigh", "max", "ultra"];
28
+ const SPEED_OPTION_IDS = ["standard", "fast"];
29
+ const ENGLISH_EXPERIENCE_OPTIONS = new Set(["chat", "quick chat", "work"]);
30
+ const ENGLISH_CONFIGURATION_AXES = {
31
+ model: "Model",
32
+ intelligence: "Intelligence",
33
+ effort: "Effort",
34
+ speed: "Speed",
35
+ power: "Power",
36
+ advanced: "Advanced"
37
+ };
38
+ const ENGLISH_CONFIGURATION_OPTIONS = {
39
+ instant: "Instant",
40
+ light: "Light",
41
+ medium: "Medium",
42
+ high: "High",
43
+ extraHigh: "Extra High",
44
+ max: "Max",
45
+ ultra: "Ultra",
46
+ pro: "Pro",
47
+ standard: "Standard",
48
+ fast: "Fast",
49
+ };
50
+ const ENGLISH_COMPOSER_LABELS = new Set(["chat with chatgpt", "ask chatgpt", "work on anything", "work on something"]);
51
+ const UPDATE_NOTE = " * Intelligence picker labels updated 2026-06-10, stop-control labels updated 2026-06-15, Chat/Work surface labels updated 2026-07-17, and Power/Advanced selector labels updated 2026-08-08 from visible ChatGPT sessions.";
17
52
  class ApplyUsageError extends Error {
18
53
  exitCode;
19
54
  constructor(message, exitCode = 2) {
@@ -58,7 +93,15 @@ export async function main(argv = process.argv.slice(2)) {
58
93
  const modeOptions = observedNonEnglishModeOptions(record);
59
94
  const stopControl = observedNonEnglishGenerationLabels(record.generationStopLabels, ENGLISH_STOP_CONTROL);
60
95
  const stoppedAssistant = observedNonEnglishGenerationLabels(record.generationStoppedLabels, ENGLISH_STOPPED_ASSISTANT);
61
- if (labels.length === 0 && Object.keys(modeOptions).length === 0 && stopControl.length === 0 && stoppedAssistant.length === 0)
96
+ const surface = observedNonEnglishSurface(record);
97
+ if (labels.length === 0
98
+ && Object.keys(modeOptions).length === 0
99
+ && stopControl.length === 0
100
+ && stoppedAssistant.length === 0
101
+ && surface.workComposerTextbox.length === 0
102
+ && Object.keys(surface.experienceOptions).length === 0
103
+ && Object.keys(surface.configurationAxes).length === 0
104
+ && Object.keys(surface.configurationOptions).length === 0)
62
105
  continue;
63
106
  planned.push({
64
107
  locale: language.bcp47,
@@ -67,6 +110,7 @@ export async function main(argv = process.argv.slice(2)) {
67
110
  modeOptions,
68
111
  stopControl,
69
112
  stoppedAssistant,
113
+ surface,
70
114
  });
71
115
  }
72
116
  if (!options.reviewed) {
@@ -81,10 +125,10 @@ export async function main(argv = process.argv.slice(2)) {
81
125
  const after = mergeCapture(before, change.labels, change.modeOptions, {
82
126
  stopControl: change.stopControl,
83
127
  stoppedAssistant: change.stoppedAssistant,
84
- });
128
+ }, change.surface);
85
129
  if (after !== before) {
86
130
  await writeFile(change.file, after, "utf8");
87
- console.log(`updated ${change.locale} labels=${change.labels.length} modeOptions=${Object.keys(change.modeOptions).length} stopControl=${change.stopControl.length} stoppedAssistant=${change.stoppedAssistant.length}`);
131
+ console.log(`updated ${change.locale} labels=${change.labels.length} modeOptions=${Object.keys(change.modeOptions).length} surfaces=${surfaceContributionCount(change.surface)} stopControl=${change.stopControl.length} stoppedAssistant=${change.stoppedAssistant.length}`);
88
132
  }
89
133
  }
90
134
  return 0;
@@ -130,9 +174,95 @@ function observedNonEnglishModeOptions(record) {
130
174
  function observedNonEnglishGenerationLabels(values, englishValues) {
131
175
  return dedupe((values ?? [])
132
176
  .map(value => value.replace(/\s+/g, " ").trim())
133
- .filter(value => value.length > 0 && !englishValues.has(value.toLowerCase())));
177
+ .filter(value => value.length > 0
178
+ && !englishValues.has(value.toLowerCase())
179
+ && !UNSAFE_GENERIC_STOP_CONTROLS.has(value.toLowerCase())));
134
180
  }
135
- export function mergeCapture(source, labels, modeOptions, generationLabels = {}) {
181
+ function observedNonEnglishSurface(record) {
182
+ const empty = {
183
+ workComposerTextbox: [],
184
+ experienceOptions: {},
185
+ configurationAxes: {},
186
+ configurationOptions: {},
187
+ };
188
+ const capture = record.surfaceCapture;
189
+ if (capture === undefined)
190
+ return empty;
191
+ if (capture.status !== "ok" || capture.chat === undefined || capture.work === undefined || !capture.restoredChat) {
192
+ throw new ApplyUsageError(`${record.requestedLocale} surface capture was not successful and restored: ${capture.blocker?.message ?? "unknown blocker"}.`, 1);
193
+ }
194
+ const contribution = { ...empty };
195
+ contribution.experienceOptions = {
196
+ ...nonEnglishSlot("chat", capture.chat.optionLabel, ENGLISH_EXPERIENCE_OPTIONS),
197
+ ...nonEnglishSlot("work", capture.work.optionLabel, ENGLISH_EXPERIENCE_OPTIONS),
198
+ };
199
+ const sharedComposerLabels = new Set(capture.chat.composerLabels.map(normalizedLower));
200
+ contribution.workComposerTextbox = dedupe(capture.work.composerLabels
201
+ .map(normalized)
202
+ .filter(label => label.length > 0
203
+ && !sharedComposerLabels.has(normalizedLower(label))
204
+ && !ENGLISH_COMPOSER_LABELS.has(normalizedLower(label))));
205
+ addLocalizedConfigurationAxis(contribution.configurationAxes, "power", capture.chat.power?.axisLabel);
206
+ addLocalizedConfigurationAxis(contribution.configurationAxes, "power", capture.work.power?.axisLabel);
207
+ addLocalizedConfigurationAxis(contribution.configurationAxes, "advanced", capture.chat.advanced?.label);
208
+ addLocalizedConfigurationAxis(contribution.configurationAxes, "advanced", capture.work.advanced?.label);
209
+ const chatRows = capture.chat.configurationRows;
210
+ if (chatRows !== undefined) {
211
+ if (chatRows.length !== CHAT_CONFIGURATION_AXIS_IDS.length
212
+ || chatRows.some((row, index) => row.axis !== CHAT_CONFIGURATION_AXIS_IDS[index])) {
213
+ throw new ApplyUsageError(`${record.requestedLocale} did not capture the ordered Chat Model/Effort rows.`, 1);
214
+ }
215
+ for (const row of chatRows) {
216
+ addLocalizedConfigurationAxis(contribution.configurationAxes, row.axis, row.axisLabel);
217
+ if (row.axis === "effort") {
218
+ assignOrderedLocalizedOptions(record.requestedLocale, row.options, CHAT_EFFORT_OPTION_IDS, contribution.configurationOptions, "Chat Effort");
219
+ }
220
+ }
221
+ }
222
+ const rows = capture.work.configurationRows;
223
+ if (rows.length !== WORK_CONFIGURATION_AXIS_IDS.length
224
+ || rows.some((row, index) => row.axis !== WORK_CONFIGURATION_AXIS_IDS[index])) {
225
+ throw new ApplyUsageError(`${record.requestedLocale} did not capture the ordered Model/Effort/Speed rows.`, 1);
226
+ }
227
+ for (const row of rows) {
228
+ addLocalizedConfigurationAxis(contribution.configurationAxes, row.axis, row.axisLabel);
229
+ if (row.axis === "effort") {
230
+ assignOrderedLocalizedOptions(record.requestedLocale, row.options, EFFORT_OPTION_IDS, contribution.configurationOptions, "Effort");
231
+ }
232
+ else if (row.axis === "speed") {
233
+ assignOrderedLocalizedOptions(record.requestedLocale, row.options, SPEED_OPTION_IDS, contribution.configurationOptions, "Speed");
234
+ }
235
+ }
236
+ return contribution;
237
+ }
238
+ function addLocalizedConfigurationAxis(target, axis, value) {
239
+ const label = normalized(value);
240
+ if (label.length === 0 || label === ENGLISH_CONFIGURATION_AXES[axis])
241
+ return;
242
+ target[axis] = dedupe([...(target[axis] ?? []), label]);
243
+ }
244
+ function assignOrderedLocalizedOptions(locale, options, ids, target, axis) {
245
+ if (options.length === 0 || options.length > ids.length) {
246
+ throw new ApplyUsageError(`${locale} captured ${options.length} ${axis} options; expected 1-${ids.length}.`, 1);
247
+ }
248
+ options.forEach((option, index) => {
249
+ const id = ids[index];
250
+ const label = normalized(option.label);
251
+ if (label.length > 0 && label !== ENGLISH_CONFIGURATION_OPTIONS[id]) {
252
+ target[id] = dedupe([...(target[id] ?? []), label]);
253
+ }
254
+ });
255
+ }
256
+ function nonEnglishSlot(id, value, englishValues) {
257
+ const label = normalized(value);
258
+ return label.length > 0 && !englishValues.has(normalizedLower(label)) ? { [id]: [label] } : {};
259
+ }
260
+ export function mergeCapture(source, labels, modeOptions, generationLabels = {}, surface = {
261
+ workComposerTextbox: [],
262
+ experienceOptions: {},
263
+ configurationAxes: {},
264
+ configurationOptions: {},
265
+ }) {
136
266
  let text = updateComment(source);
137
267
  if (labels.length > 0) {
138
268
  const existing = parseExistingModeLabels(text);
@@ -149,6 +279,10 @@ export function mergeCapture(source, labels, modeOptions, generationLabels = {})
149
279
  }
150
280
  }
151
281
  text = mergeModeOptions(text, modeOptions);
282
+ text = mergeTopLevelStringArrayProperty(text, "workComposerTextbox", surface.workComposerTextbox, "composerTextbox");
283
+ text = mergeNestedOptions(text, "experienceOptions", EXPERIENCE_OPTION_IDS, surface.experienceOptions, "modeOpenerExtra");
284
+ text = mergeNestedOptions(text, "configurationAxes", CONFIGURATION_AXIS_IDS, surface.configurationAxes, "experienceOptions");
285
+ text = mergeNestedOptions(text, "configurationOptions", CONFIGURATION_OPTION_IDS, surface.configurationOptions, "configurationAxes");
152
286
  text = mergeStringArrayProperty(text, "stopControl", generationLabels.stopControl ?? []);
153
287
  text = mergeStringArrayProperty(text, "stoppedAssistant", generationLabels.stoppedAssistant ?? []);
154
288
  return text;
@@ -220,13 +354,71 @@ function formatModeOptions(modeOptions) {
220
354
  }
221
355
  return [" modeOptions: {", ...lines, " },"].join("\n");
222
356
  }
223
- function mergeStringArrayProperty(source, property, values) {
357
+ function mergeTopLevelStringArrayProperty(source, property, values, insertAfterProperty) {
224
358
  if (values.length === 0)
225
359
  return source;
226
360
  const existing = parseExistingStringArrayProperty(source, property);
227
361
  const merged = dedupe([...existing, ...values]);
228
362
  const line = ` ${property}: [${merged.map(value => JSON.stringify(value)).join(", ")}],`;
229
363
  const propertyPattern = new RegExp(`^\\s*${property}:\\s*\\[[^\\]]*\\],`, "m");
364
+ if (propertyPattern.test(source))
365
+ return source.replace(propertyPattern, line);
366
+ const anchor = new RegExp(`^(\\s*${insertAfterProperty}:\\s*[^\\n]+,\\n)`, "m");
367
+ if (anchor.test(source))
368
+ return source.replace(anchor, `$1${line}\n`);
369
+ return source.replace(/^export const \w+ = \{\n/m, match => `${match}${line}\n`);
370
+ }
371
+ function mergeNestedOptions(source, property, ids, additions, insertAfterProperty) {
372
+ const existing = parseNestedOptions(source, property, ids);
373
+ const merged = {};
374
+ for (const id of ids) {
375
+ const values = dedupe([...(existing[id] ?? []), ...(additions[id] ?? [])]);
376
+ if (values.length > 0)
377
+ merged[id] = values;
378
+ }
379
+ const lines = ids.flatMap(id => {
380
+ const values = merged[id];
381
+ return values === undefined || values.length === 0
382
+ ? []
383
+ : [` ${id}: [${values.map(value => JSON.stringify(value)).join(", ")}],`];
384
+ });
385
+ if (lines.length === 0)
386
+ return source;
387
+ const block = [` ${property}: {`, ...lines, " },"].join("\n");
388
+ const blockPattern = new RegExp(`^\\s*${property}:\\s*\\{[\\s\\S]*?^\\s*\\},\\n`, "m");
389
+ if (blockPattern.test(source))
390
+ return source.replace(blockPattern, `${block}\n`);
391
+ const nestedAnchor = new RegExp(`^(\\s*${insertAfterProperty}:\\s*\\{[\\s\\S]*?^\\s*\\},\\n)`, "m");
392
+ if (nestedAnchor.test(source))
393
+ return source.replace(nestedAnchor, `$1${block}\n`);
394
+ const lineAnchor = new RegExp(`^(\\s*${insertAfterProperty}:\\s*[^\\n]+,\\n)`, "m");
395
+ if (lineAnchor.test(source))
396
+ return source.replace(lineAnchor, `$1${block}\n`);
397
+ return source.replace(/^export const \w+ = \{\n/m, match => `${match}${block}\n`);
398
+ }
399
+ function parseNestedOptions(source, property, ids) {
400
+ const result = {};
401
+ const block = new RegExp(`^\\s*${property}:\\s*\\{(?<body>[\\s\\S]*?)^\\s*\\},`, "m").exec(source)?.groups?.body;
402
+ if (block === undefined)
403
+ return result;
404
+ for (const id of ids) {
405
+ const body = new RegExp(`^\\s*${id}:\\s*\\[(?<body>[^\\]]*)\\],`, "m").exec(block)?.groups?.body;
406
+ if (body === undefined)
407
+ continue;
408
+ const values = parseJsonStringList(body);
409
+ if (values.length > 0)
410
+ result[id] = values;
411
+ }
412
+ return result;
413
+ }
414
+ function mergeStringArrayProperty(source, property, values) {
415
+ if (values.length === 0)
416
+ return source;
417
+ const existing = parseExistingStringArrayProperty(source, property);
418
+ const merged = dedupe([...existing, ...values])
419
+ .filter(value => property !== "stopControl" || !UNSAFE_GENERIC_STOP_CONTROLS.has(value.toLowerCase()));
420
+ const line = ` ${property}: [${merged.map(value => JSON.stringify(value)).join(", ")}],`;
421
+ const propertyPattern = new RegExp(`^\\s*${property}:\\s*\\[[^\\]]*\\],`, "m");
230
422
  if (propertyPattern.test(source)) {
231
423
  return source.replace(propertyPattern, line);
232
424
  }
@@ -251,11 +443,31 @@ function parseExistingStringArrayProperty(source, property) {
251
443
  }
252
444
  function updateComment(source) {
253
445
  let text = source.replace(/\n \* Omitted because they match English case-insensitively: `modeLabels`[\s\S]*?blocker copy\.\n/g, "\n * Some non-Intelligence surfaces may still fall back to English + `selector_drift`.\n");
446
+ text = text.replace(/^ \* Intelligence picker labels updated 2026-06-10[^\n]*$/m, UPDATE_NOTE);
254
447
  if (!text.includes(UPDATE_NOTE)) {
255
448
  text = text.replace(/\n \*\//, `\n *\n${UPDATE_NOTE}\n */`);
256
449
  }
257
450
  return text;
258
451
  }
452
+ function parseJsonStringList(body) {
453
+ const values = [];
454
+ for (const stringMatch of body.matchAll(/"((?:\\"|[^"])*)"/g)) {
455
+ values.push(JSON.parse(`"${stringMatch[1]}"`));
456
+ }
457
+ return values;
458
+ }
459
+ function normalized(value) {
460
+ return (value ?? "").replace(/\s+/g, " ").trim();
461
+ }
462
+ function normalizedLower(value) {
463
+ return normalized(value).toLocaleLowerCase();
464
+ }
465
+ function surfaceContributionCount(surface) {
466
+ return surface.workComposerTextbox.length
467
+ + Object.keys(surface.experienceOptions).length
468
+ + Object.keys(surface.configurationAxes).length
469
+ + Object.keys(surface.configurationOptions).length;
470
+ }
259
471
  function dedupe(values) {
260
472
  const seen = new Set();
261
473
  const result = [];