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
@@ -33,6 +33,8 @@ const REQUIRED_LOCALE_KEYS = [
33
33
  ];
34
34
  const REQUIRED_TOOL_IDS = ["web_search", "deep_research", "create_image"];
35
35
  const REQUIRED_MODE_OPTION_IDS = ["latest", "instant", "thinking", "extended", "medium", "high", "extraHigh", "pro"];
36
+ const REQUIRED_CONFIGURATION_AXIS_IDS = ["power", "model", "intelligence", "effort", "speed", "advanced"];
37
+ const REQUIRED_CONFIGURATION_OPTION_IDS = ["instant", "light", "medium", "high", "extraHigh", "max", "ultra", "pro", "standard", "fast"];
36
38
  export async function doctor(env, args = {}) {
37
39
  const wanted = args.check ?? DEFAULT_CHECKS;
38
40
  const checks = {};
@@ -232,8 +234,14 @@ function localizationCheck(env) {
232
234
  const requiredKeysMissing = REQUIRED_LOCALE_KEYS.filter(key => localeLabels[key].length === 0);
233
235
  const missingToolIds = REQUIRED_TOOL_IDS.filter(id => (localeLabels.tools[id]?.length ?? 0) === 0);
234
236
  const missingModeOptionIds = REQUIRED_MODE_OPTION_IDS.filter(id => (localeLabels.modeOptions[id]?.length ?? 0) === 0);
237
+ const missingConfigurationAxisIds = REQUIRED_CONFIGURATION_AXIS_IDS
238
+ .filter(id => (localeLabels.configurationAxes[id]?.length ?? 0) === 0);
239
+ const missingConfigurationOptionIds = REQUIRED_CONFIGURATION_OPTION_IDS
240
+ .filter(id => (localeLabels.configurationOptions[id]?.length ?? 0) === 0);
235
241
  const toolIds = Object.keys(localeLabels.tools);
236
242
  const modeOptionIds = Object.keys(localeLabels.modeOptions);
243
+ const configurationAxisIds = Object.keys(localeLabels.configurationAxes);
244
+ const configurationOptionIds = Object.keys(localeLabels.configurationOptions);
237
245
  const proAliases = [...localeLabels.modeOptions.pro];
238
246
  const supportsProExtendedAlias = proAliases.some(alias => /pro\s*(?:•\s*)?extended/i.test(alias))
239
247
  && proAliases.some(alias => /extended\s+pro/i.test(alias));
@@ -250,17 +258,26 @@ function localizationCheck(env) {
250
258
  && localeLabels.sendButton[0] === "Send prompt"
251
259
  && localeLabels.modeLabels.includes("Thinking")
252
260
  && localeLabels.modeOptions.pro?.[0] === "Pro"
261
+ && localeLabels.configurationAxes.power?.[0] === "Power"
262
+ && localeLabels.configurationAxes.advanced?.[0] === "Advanced"
263
+ && localeLabels.configurationOptions.ultra?.[0] === "Ultra"
253
264
  && localeLabels.tools.web_search?.[0] === "Web search";
254
265
  const labelCandidateCount = REQUIRED_LOCALE_KEYS.reduce((total, key) => total + localeLabels[key].length, 0)
255
266
  + Object.values(localeLabels.tools).reduce((total, values) => total + values.length, 0)
256
- + Object.values(localeLabels.modeOptions).reduce((total, values) => total + values.length, 0);
267
+ + Object.values(localeLabels.modeOptions).reduce((total, values) => total + values.length, 0)
268
+ + Object.values(localeLabels.configurationAxes).reduce((total, values) => total + values.length, 0)
269
+ + Object.values(localeLabels.configurationOptions).reduce((total, values) => total + values.length, 0);
257
270
  const details = {
258
271
  englishCanonicalPresent,
259
272
  requiredKeysMissing,
260
273
  missingToolIds,
261
274
  missingModeOptionIds,
275
+ missingConfigurationAxisIds,
276
+ missingConfigurationOptionIds,
262
277
  toolIds,
263
278
  modeOptionIds,
279
+ configurationAxisIds,
280
+ configurationOptionIds,
264
281
  proAliases,
265
282
  supportsProExtendedAlias,
266
283
  runningStateLabelCoverage,
@@ -268,7 +285,12 @@ function localizationCheck(env) {
268
285
  pageAvailable: env.page !== undefined,
269
286
  runtimeSelectorCoverage: "registry_only_stage_2"
270
287
  };
271
- if (englishCanonicalPresent && requiredKeysMissing.length === 0 && missingToolIds.length === 0 && missingModeOptionIds.length === 0) {
288
+ if (englishCanonicalPresent
289
+ && requiredKeysMissing.length === 0
290
+ && missingToolIds.length === 0
291
+ && missingModeOptionIds.length === 0
292
+ && missingConfigurationAxisIds.length === 0
293
+ && missingConfigurationOptionIds.length === 0) {
272
294
  const runningStateMessage = runningStateLocalizationSupport === "complete"
273
295
  ? "running-state labels are present for every registered non-English locale"
274
296
  : runningStateLocalizationSupport === "partial"
@@ -4,6 +4,7 @@ type SurfaceSnapshot = {
4
4
  composerLabels: string[];
5
5
  mainControls: string[];
6
6
  mainText: string;
7
+ selectedSurfaceLabels?: string[];
7
8
  };
8
9
  export declare function detectExperience(env: RuntimeEnv, args?: DetectExperienceArgs): Promise<CommandResult<DetectExperienceData>>;
9
10
  export declare function openExperience(env: RuntimeEnv, args: OpenExperienceArgs): Promise<CommandResult<OpenExperienceData>>;
@@ -3,6 +3,9 @@ import { localeLabels } from "../dom/locale-labels.js";
3
3
  import { normalizeForLabelMatch, visibleLabelMatches } from "../dom/label-match.js";
4
4
  import { contextFromPage } from "./context.js";
5
5
  import { ensurePage } from "./session.js";
6
+ const CHATGPT_HOME = "https://chatgpt.com/";
7
+ const EXPERIENCE_CONTROL_DISCOVERY_TIMEOUT_MS = 15_000;
8
+ const EXPERIENCE_POLL_MS = 250;
6
9
  export async function detectExperience(env, args = {}) {
7
10
  void args;
8
11
  const boot = await ensurePage(env);
@@ -43,14 +46,50 @@ export async function openExperience(env, args) {
43
46
  }));
44
47
  }
45
48
  const labels = localeLabels.experienceOptions[args.experience];
46
- if (!await clickUniqueExperienceControl(page, labels)) {
47
- return experienceSelectorDrift(page, `No unique visible ChatGPT ${args.experience === "work" ? "Work" : "Chat"} surface control was found.`, before);
48
- }
49
49
  const timeoutMs = args.timeoutMs ?? 30000;
50
- const started = Date.now();
50
+ const discoveryAttempts = pollAttempts(Math.min(timeoutMs, EXPERIENCE_CONTROL_DISCOVERY_TIMEOUT_MS), EXPERIENCE_POLL_MS);
51
+ let observed = before;
52
+ let controlClicked = await clickUniqueExperienceControl(page, labels);
53
+ if (!controlClicked && await navigateConversationToSurfaceHome(page, args.timeoutMs)) {
54
+ observed = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
55
+ if (observed.experience === args.experience) {
56
+ return resultOk({
57
+ experience: args.experience,
58
+ previousExperience: before.experience,
59
+ changed: true,
60
+ selectorProfile: observed.selectorProfile
61
+ }, await contextFromPage(page, {
62
+ experience: observed.experience,
63
+ selectorProfile: observed.selectorProfile
64
+ }));
65
+ }
66
+ controlClicked = await clickUniqueExperienceControl(page, labels);
67
+ }
68
+ // session.bootstrap can verify the composer before the Chat/Work radio has
69
+ // hydrated. Give the scoped surface control a short bounded discovery
70
+ // window instead of reporting selector drift from that transient state.
71
+ for (let attempt = 1; !controlClicked && attempt < discoveryAttempts; attempt += 1) {
72
+ await page.waitForTimeout?.(EXPERIENCE_POLL_MS);
73
+ observed = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
74
+ if (observed.experience === args.experience) {
75
+ return resultOk({
76
+ experience: args.experience,
77
+ previousExperience: before.experience,
78
+ changed: true,
79
+ selectorProfile: observed.selectorProfile
80
+ }, await contextFromPage(page, {
81
+ experience: observed.experience,
82
+ selectorProfile: observed.selectorProfile
83
+ }));
84
+ }
85
+ controlClicked = await clickUniqueExperienceControl(page, labels);
86
+ }
87
+ if (!controlClicked) {
88
+ return experienceSelectorDrift(page, `No unique visible ChatGPT ${args.experience === "work" ? "Work" : "Chat"} surface control was found.`, observed);
89
+ }
51
90
  let after = before;
52
- while (Date.now() - started < timeoutMs) {
53
- await page.waitForTimeout?.(250);
91
+ for (let attempt = 0; attempt < pollAttempts(timeoutMs, EXPERIENCE_POLL_MS); attempt += 1) {
92
+ await page.waitForTimeout?.(EXPERIENCE_POLL_MS);
54
93
  after = detectExperienceFromSnapshot(await readSurfaceSnapshot(page));
55
94
  if (after.experience === args.experience) {
56
95
  return resultOk({
@@ -86,12 +125,39 @@ export async function openExperience(env, args) {
86
125
  return resultError(error instanceof Error ? error : new Error(String(error)), await contextFromPage(page));
87
126
  }
88
127
  }
128
+ function pollAttempts(timeoutMs, pollMs) {
129
+ return Math.max(1, Math.ceil(Math.max(0, timeoutMs) / pollMs));
130
+ }
131
+ async function navigateConversationToSurfaceHome(page, timeoutMs) {
132
+ if (page.goto === undefined || page.url === undefined)
133
+ return false;
134
+ const currentUrl = await Promise.resolve(page.url()).catch(() => "");
135
+ if (!/^https:\/\/chatgpt\.com\/c\//i.test(currentUrl))
136
+ return false;
137
+ await page.goto(CHATGPT_HOME, {
138
+ waitUntil: "domcontentloaded",
139
+ timeout: timeoutMs ?? 30000
140
+ });
141
+ await page.waitForTimeout?.(500);
142
+ return true;
143
+ }
89
144
  export function detectExperienceFromSnapshot(snapshot) {
90
145
  const evidence = [];
91
146
  const composerLabels = snapshot.composerLabels.map(normalizeForLabelMatch);
92
147
  const controls = snapshot.mainControls.map(normalizeForLabelMatch);
93
148
  const mainText = normalizeForLabelMatch(snapshot.mainText);
149
+ const selectedSurfaceLabels = (snapshot.selectedSurfaceLabels ?? []).map(normalizeForLabelMatch);
94
150
  const url = snapshot.url.toLowerCase();
151
+ const selectedWork = matchingLabels(selectedSurfaceLabels, localeLabels.experienceOptions.work);
152
+ const selectedChat = matchingLabels(selectedSurfaceLabels, localeLabels.experienceOptions.chat);
153
+ const workSurfaceSelected = selectedWork.length > 0 && selectedChat.length === 0;
154
+ const chatSurfaceSelected = selectedChat.length > 0 && selectedWork.length === 0;
155
+ if (workSurfaceSelected) {
156
+ evidence.push({ source: "control", label: "Work surface selected" });
157
+ }
158
+ else if (chatSurfaceSelected) {
159
+ evidence.push({ source: "control", label: "Chat surface selected" });
160
+ }
95
161
  const workComposer = matchingLabels(composerLabels, localeLabels.workComposerTextbox);
96
162
  for (const label of workComposer) {
97
163
  evidence.push({ source: "composer", label });
@@ -106,6 +172,18 @@ export function detectExperienceFromSnapshot(snapshot) {
106
172
  if (workAxisCount >= 2) {
107
173
  evidence.push({ source: "control", label: `Work configuration axes (${workAxisCount}/3)` });
108
174
  }
175
+ const workConfigurationOpener = controls.some(label => /\b(?:gpt[\s-]?\d|\d+(?:\.\d+)+|sol|luna|terra)\b/i.test(label)
176
+ && hasAnyLabel([label], [
177
+ ...localeLabels.configurationOptions.light,
178
+ ...localeLabels.configurationOptions.medium,
179
+ ...localeLabels.configurationOptions.high,
180
+ ...localeLabels.configurationOptions.extraHigh,
181
+ ...localeLabels.configurationOptions.max,
182
+ ...localeLabels.configurationOptions.ultra,
183
+ ]));
184
+ if (workConfigurationOpener) {
185
+ evidence.push({ source: "control", label: "Work configuration opener" });
186
+ }
109
187
  if (/\/work(?:\/|$|\?)/.test(url)) {
110
188
  evidence.push({ source: "url", label: snapshot.url });
111
189
  }
@@ -113,15 +191,21 @@ export function detectExperienceFromSnapshot(snapshot) {
113
191
  evidence.push({ source: "heading", label: "Work composer copy" });
114
192
  }
115
193
  const workScore = workComposer.length * 4
194
+ + (workSurfaceSelected ? 10 : 0)
116
195
  + (workAxisCount >= 2 ? 4 : 0)
196
+ // The active Work task drops the Chat/Work radio and keeps the shared
197
+ // "Chat with ChatGPT" textbox name. Its compound model + effort opener is
198
+ // therefore strong enough to disambiguate that continuation surface.
199
+ + (workConfigurationOpener ? 6 : 0)
117
200
  + (/\/work(?:\/|$|\?)/.test(url) ? 3 : 0)
118
201
  + (containsAny(mainText, ["work on something else", "work on anything"]) ? 2 : 0);
119
- const chatScore = chatComposer.length * 4;
202
+ const chatScore = chatComposer.length * 4
203
+ + (chatSurfaceSelected ? 10 : 0);
120
204
  let experience = "unknown";
121
205
  let confidence = "low";
122
206
  if (workScore > chatScore && workScore >= 4) {
123
207
  experience = "work";
124
- confidence = workScore >= 7 ? "high" : "medium";
208
+ confidence = workSurfaceSelected || workScore >= 7 ? "high" : "medium";
125
209
  }
126
210
  else if (chatScore > workScore && chatScore >= 4) {
127
211
  experience = "chat";
@@ -135,17 +219,28 @@ export async function readSurfaceSnapshot(page) {
135
219
  ? await Promise.resolve(page.url()).catch(() => "")
136
220
  : "";
137
221
  if (typeof page.evaluate !== "function") {
138
- return { url, composerLabels: [], mainControls: [], mainText: "" };
222
+ return { url, composerLabels: [], mainControls: [], mainText: "", selectedSurfaceLabels: [] };
139
223
  }
140
- const snapshot = await page.evaluate(() => {
224
+ const snapshot = await page.evaluate((surfaceOptionLabels) => {
141
225
  const visible = (element) => {
142
226
  const html = element;
143
227
  const rect = html.getBoundingClientRect?.();
144
- const style = typeof window !== "undefined" ? window.getComputedStyle?.(html) : undefined;
145
- return (rect === undefined || (rect.width > 0 && rect.height > 0))
146
- && style?.display !== "none"
147
- && style?.visibility !== "hidden"
148
- && style?.opacity !== "0";
228
+ if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
229
+ return false;
230
+ let current = element;
231
+ while (current !== null) {
232
+ if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
233
+ return false;
234
+ }
235
+ const style = typeof window !== "undefined"
236
+ ? window.getComputedStyle?.(current)
237
+ : undefined;
238
+ if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
239
+ return false;
240
+ }
241
+ current = current.parentElement ?? null;
242
+ }
243
+ return true;
149
244
  };
150
245
  const labelFor = (element) => {
151
246
  const html = element;
@@ -156,6 +251,8 @@ export async function readSurfaceSnapshot(page) {
156
251
  ?? "";
157
252
  };
158
253
  const normalize = (value) => value.replace(/\s+/g, " ").trim();
254
+ const normalizeComparable = (value) => normalize(value).toLocaleLowerCase();
255
+ const wantedSurfaceLabels = new Set(surfaceOptionLabels.map(normalizeComparable));
159
256
  const composerRoots = Array.from(document.querySelectorAll("main form, main [data-testid*='composer' i], main [class*='composer' i]"));
160
257
  const composerNodes = composerRoots.flatMap(root => [
161
258
  root,
@@ -183,8 +280,17 @@ export async function readSurfaceSnapshot(page) {
183
280
  .filter(visible)
184
281
  .slice(0, 32);
185
282
  const mainText = normalize(surfaceTextNodes.map(labelFor).join(" ")).slice(0, 2000);
186
- return { composerLabels, mainControls, mainText };
187
- }).catch(() => ({ composerLabels: [], mainControls: [], mainText: "" }));
283
+ const selectedSurfaceLabels = Array.from(new Set(Array.from(document.querySelectorAll("[role='radio'][aria-checked='true'], [role='radio'][data-state='checked'], input[type='radio']:checked"))
284
+ .filter(visible)
285
+ .map(labelFor)
286
+ .map(normalize)
287
+ .filter(label => wantedSurfaceLabels.has(normalizeComparable(label)))))
288
+ .slice(0, 4);
289
+ return { composerLabels, mainControls, mainText, selectedSurfaceLabels };
290
+ }, [
291
+ ...localeLabels.experienceOptions.chat,
292
+ ...localeLabels.experienceOptions.work,
293
+ ]).catch(() => ({ composerLabels: [], mainControls: [], mainText: "", selectedSurfaceLabels: [] }));
188
294
  return { url, ...snapshot };
189
295
  }
190
296
  function profileFromSnapshot(snapshot, experience) {
@@ -220,7 +326,7 @@ function profileFromSnapshot(snapshot, experience) {
220
326
  }
221
327
  async function clickUniqueExperienceControl(page, labels) {
222
328
  for (const label of labels) {
223
- for (const role of ["button", "menuitem", "tab", "link"]) {
329
+ for (const role of ["radio", "button", "menuitem", "tab", "link"]) {
224
330
  if (await clickIfUnique(page.getByRole?.(role, { name: label, exact: true }))) {
225
331
  return true;
226
332
  }
@@ -232,12 +338,39 @@ async function clickUniqueExperienceControl(page, labels) {
232
338
  return page.evaluate((wantedLabels) => {
233
339
  const normalize = (value) => value.replace(/\s+/g, " ").trim().toLocaleLowerCase();
234
340
  const wanted = new Set(wantedLabels.map(normalize));
235
- const nodes = Array.from(document.querySelectorAll("header button, header [role='button'], header [role='tab'], main [role='menuitem'], main [role='option']"));
236
- const matches = nodes.filter(node => {
341
+ const visible = (element) => {
342
+ const html = element;
343
+ const rect = html.getBoundingClientRect?.();
344
+ if (rect !== undefined && (rect.width <= 0 || rect.height <= 0))
345
+ return false;
346
+ let current = element;
347
+ while (current !== null) {
348
+ if (current.hasAttribute?.("inert") || current.getAttribute?.("aria-hidden") === "true") {
349
+ return false;
350
+ }
351
+ const style = typeof window !== "undefined"
352
+ ? window.getComputedStyle?.(current)
353
+ : undefined;
354
+ if (style?.display === "none" || style?.visibility === "hidden" || style?.opacity === "0") {
355
+ return false;
356
+ }
357
+ current = current.parentElement ?? null;
358
+ }
359
+ return true;
360
+ };
361
+ const labelFor = (node) => {
237
362
  const html = node;
238
- const label = node.getAttribute("aria-label") ?? html.innerText ?? node.textContent ?? "";
239
- return wanted.has(normalize(label));
240
- });
363
+ const inputLabels = "labels" in node
364
+ ? Array.from(node.labels ?? []).map(label => label.innerText).join(" ")
365
+ : "";
366
+ return node.getAttribute("aria-label")
367
+ || inputLabels
368
+ || html.innerText
369
+ || node.textContent
370
+ || "";
371
+ };
372
+ const nodes = Array.from(document.querySelectorAll("[role='radio'], input[type='radio'], header button, header [role='button'], header [role='tab'], main [role='menuitem'], main [role='option']"));
373
+ const matches = nodes.filter(node => visible(node) && wanted.has(normalize(labelFor(node))));
241
374
  if (matches.length !== 1)
242
375
  return false;
243
376
  matches[0].click();
@@ -3,3 +3,4 @@ export declare function validateAttachPaths(paths: string[]): Promise<AttachedFi
3
3
  export declare function preflightFiles(env: RuntimeEnv, args: FilePreflightArgs): Promise<CommandResult<FilePreflightData>>;
4
4
  export declare function attachFiles(env: RuntimeEnv, args: AttachFilesArgs): Promise<CommandResult<AttachFilesData>>;
5
5
  export declare function downloadLatestFile(env: RuntimeEnv, args: DownloadLatestArgs): Promise<CommandResult<DownloadedFile>>;
6
+ export declare function stripLocalizedDownloadPrefix(value: string, labels: readonly string[]): string;